Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 164 additions & 46 deletions src/pix_framework/io/bpm_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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]:
Expand All @@ -306,17 +336,23 @@ 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)

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:
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
3 changes: 3 additions & 0 deletions tests/pix_framework/assets/and_xor_event_log.csv
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading