Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 109f81e

Browse files
committedJun 5, 2024·
Clean up
1 parent 9d22b45 commit 109f81e

File tree

3 files changed

+10
-689
lines changed

3 files changed

+10
-689
lines changed
 

‎explainer/explainer.py

-369
Original file line numberDiff line numberDiff line change
@@ -105,375 +105,6 @@ def __iter__(self):
105105
yield Trace(list(trace_tuple))
106106

107107

108-
class Explainer:
109-
def __init__(self):
110-
"""
111-
Initializes an Explainer instance.
112-
"""
113-
self.constraints = [] # List to store constraints (regex patterns)
114-
self.adherent_trace = None
115-
116-
def add_constraint(self, regex):
117-
"""
118-
Adds a new constraint and updates the nodes list.
119-
120-
:param regex: A regular expression representing the constraint.
121-
"""
122-
self.constraints.append(regex)
123-
if self.contradiction():
124-
self.constraints.remove(regex)
125-
print(f"Constraint {regex} contradicts the other constraints.")
126-
127-
def remove_constraint(self, idx):
128-
"""
129-
Removes a constraint by index and updates the nodes list if necessary.
130-
131-
:param idx: Index of the constraint to be removed.
132-
"""
133-
if 0 <= idx < len(self.constraints):
134-
removed_regex = self.constraints.pop(idx)
135-
removed_nodes = set(filter(str.isalpha, removed_regex))
136-
137-
# Re-evaluate nodes to keep based on remaining constraints
138-
remaining_nodes = set(filter(str.isalpha, "".join(self.constraints)))
139-
self.nodes = remaining_nodes
140-
141-
# Optionally, remove nodes that are no longer in any constraint
142-
for node in removed_nodes:
143-
if node not in remaining_nodes:
144-
self.nodes.discard(node)
145-
146-
def activation(self, trace, constraints=None):
147-
"""
148-
Checks if any of the nodes in the trace activates any constraint.
149-
150-
:param trace: A Trace instance.
151-
:return: Boolean indicating if any constraint is activated.
152-
"""
153-
if not constraints:
154-
constraints = self.constraints
155-
con_activation = [0] * len(constraints)
156-
activated = False
157-
for idx, con in enumerate(constraints):
158-
if activated:
159-
activated = False
160-
continue
161-
target = self.identify_existance_constraints(con)
162-
if target:
163-
con_activation[idx] = 1
164-
continue
165-
for event in trace:
166-
if event in con:
167-
con_activation[idx] = 1
168-
activated = True
169-
break
170-
return con_activation
171-
172-
def identify_existance_constraints(self, pattern):
173-
"""
174-
Identifies existance constraints within a pattern.
175-
176-
:param pattern: The constraint pattern as a string.
177-
:return: A tuple indicating the type of existance constraint and the node involved.
178-
"""
179-
# Check for AtLeastOne constraint
180-
for match in re.finditer(r"(?<!^)(.)\.\*", pattern):
181-
return "ALO, " f"{match.group(1)}"
182-
183-
# Check for End constraint
184-
end_match = re.search(r"(.)\$(?=\Z|\))", pattern)
185-
if end_match:
186-
return "E", f"{end_match.group(1)}"
187-
# Check for Init constraint
188-
init_match = re.match(r"(?:\A\^|\((?:\?[^)]+\))?\^)(.)", pattern)
189-
if init_match:
190-
return ("I", f"{init_match.group(1)}")
191-
return None
192-
193-
def conformant(self, trace, constraints=None):
194-
"""
195-
Checks if the trace is conformant according to all the constraints.
196-
197-
:param trace: A Trace instance.
198-
:return: Boolean indicating if the trace is conformant with all constraints.
199-
"""
200-
activation = self.activation(trace, constraints)
201-
if any(value == 0 for value in activation):
202-
new_explainer = Explainer()
203-
for idx, value in enumerate(activation):
204-
if value == 1:
205-
new_explainer.add_constraint(self.constraints[idx])
206-
return new_explainer.conformant(trace)
207-
trace_str = "".join(trace)
208-
if constraints:
209-
return all(re.search(constraint, trace_str) for constraint in constraints)
210-
return all(re.search(constraint, trace_str) for constraint in self.constraints)
211-
212-
def contradiction(self):
213-
"""
214-
Checks if there is a contradiction among the constraints.
215-
216-
:return: Boolean indicating if there is a contradiction.
217-
"""
218-
nodes = self.get_nodes_from_constraint()
219-
max_length = 10 # Set a reasonable max length to avoid infinite loops
220-
nodes = nodes + nodes
221-
for length in range(1, max_length + 1):
222-
for combination in product(nodes, repeat=length):
223-
test_str = "".join(combination)
224-
if all(re.search(con, test_str) for con in self.constraints):
225-
self.adherent_trace = test_str
226-
return False # Found a match
227-
return True # No combination satisfied all constraints
228-
229-
def minimal_expl(self, trace):
230-
"""
231-
Provides a minimal explanation for non-conformance, given the trace and constraints.
232-
233-
:param trace: A Trace instance.
234-
:return: Explanation of why the trace is non-conformant.
235-
"""
236-
237-
# Because constraints that are not activated should not be considered we create a new explainer with the relevant constraints in this case
238-
activation = self.activation(trace)
239-
if any(value == 0 for value in activation):
240-
new_explainer = Explainer()
241-
for idx, value in enumerate(activation):
242-
if value == 1:
243-
new_explainer.add_constraint(self.constraints[idx])
244-
return new_explainer.minimal_expl(trace)
245-
246-
if self.conformant(trace):
247-
return "The trace is already conformant, no changes needed."
248-
explanations = None
249-
250-
for constraint in self.constraints:
251-
for subtrace in get_sublists(trace):
252-
trace_str = "".join(subtrace)
253-
if not re.search(constraint, trace_str):
254-
explanations = (
255-
f"Constraint ({constraint}) is violated by subtrace: {subtrace}"
256-
)
257-
break
258-
259-
if explanations:
260-
return "Non-conformance due to: " + explanations
261-
else:
262-
return "Trace is non-conformant, but the specific constraint violation could not be determined."
263-
264-
def counterfactual_expl(self, trace):
265-
"""
266-
Generates a counterfactual explanation for a given trace.
267-
268-
:param trace: The trace to be explained.
269-
:return: A string explaining why the trace is non-conformant or a message indicating no changes are needed.
270-
"""
271-
activation = self.activation(trace)
272-
if any(value == 0 for value in activation):
273-
new_explainer = Explainer()
274-
for idx, value in enumerate(activation):
275-
if value == 1:
276-
new_explainer.add_constraint(self.constraints[idx])
277-
return new_explainer.counterfactual_expl(trace)
278-
279-
if self.conformant(trace):
280-
return "The trace is already conformant, no changes needed."
281-
score = self.evaluate_similarity(trace)
282-
# Perform operation based on the lowest scoring heuristic
283-
return self.operate_on_trace(trace, score, "")
284-
285-
def counter_factual_helper(self, working_trace, explanation, depth=0):
286-
"""
287-
Recursively explores counterfactual explanations for a working trace.
288-
289-
:param working_trace: The trace being explored.
290-
:param explanation: The current explanation path.
291-
:param depth: The current recursion depth.
292-
:return: A string explaining why the working trace is non-conformant or a message indicating the maximum depth has been reached.
293-
"""
294-
if self.conformant(working_trace):
295-
return f"{explanation}"
296-
if depth > 100:
297-
return f"{explanation}\n Maximum depth of {depth -1} reached"
298-
score = self.evaluate_similarity(working_trace)
299-
return self.operate_on_trace(working_trace, score, explanation, depth)
300-
301-
def operate_on_trace(self, trace, score, explanation_path, depth=0):
302-
"""
303-
Finds and applies modifications to the trace to make it conformant.
304-
305-
:param trace: The trace to be modified.
306-
:param score: The similarity score of the trace.
307-
:param explanation_path: The current explanation path.
308-
:param depth: The current recursion depth.
309-
:return: A string explaining why the best subtrace is non-conformant or a message indicating the maximum depth has been reached.
310-
"""
311-
explanation = None
312-
counter_factuals = self.modify_subtrace(trace)
313-
best_subtrace = None
314-
best_score = -float("inf")
315-
for subtrace in counter_factuals:
316-
current_score = self.evaluate_similarity(subtrace[0])
317-
if current_score > best_score and current_score > score:
318-
best_score = current_score
319-
best_subtrace = subtrace[0]
320-
explanation = subtrace[1]
321-
if best_subtrace == None:
322-
for subtrace in counter_factuals:
323-
self.operate_on_trace(subtrace[0], score, explanation_path, depth + 1)
324-
explanation_string = explanation_path + "\n" + str(explanation)
325-
return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1)
326-
327-
def get_nodes_from_constraint(self, constraint=None):
328-
"""
329-
Extracts unique nodes from a constraint pattern.
330-
331-
:param constraint: The constraint pattern as a string.
332-
:return: A list of unique nodes found within the constraint.
333-
"""
334-
if constraint is None:
335-
all_nodes = set()
336-
for con in self.constraints:
337-
all_nodes.update(re.findall(r"[A-Za-z]", con))
338-
return list(set(all_nodes))
339-
else:
340-
return list(set(re.findall(r"[A-Za-z]", constraint)))
341-
342-
def modify_subtrace(self, trace):
343-
"""
344-
Modifies the given trace to meet constraints by adding nodes where the pattern fails.
345-
346-
Parameters:
347-
- trace: A list of node identifiers
348-
349-
Returns:
350-
- A list of potential subtraces each modified to meet constraints.
351-
"""
352-
potential_subtraces = []
353-
possible_additions = self.get_nodes_from_constraint()
354-
for i, s_trace in enumerate(get_iterative_subtrace(trace)):
355-
for con in self.constraints:
356-
new_trace_str = "".join(s_trace)
357-
match = re.match(new_trace_str, con)
358-
if not match:
359-
for add in possible_additions:
360-
potential_subtraces.append(
361-
[
362-
Trace(s_trace + [add] + trace.nodes[i + 1 :]),
363-
f"Addition (Added {add} at position {i+1}): "
364-
+ "->".join(s_trace + [add] + trace.nodes[i + 1 :]),
365-
]
366-
)
367-
potential_subtraces.append(
368-
[
369-
Trace(s_trace[:-1] + [add] + trace.nodes[i:]),
370-
f"Addition (Added {add} at position {i}): "
371-
+ "->".join(s_trace[:-1] + [add] + trace.nodes[i:]),
372-
]
373-
)
374-
375-
potential_subtraces.append(
376-
[
377-
Trace(s_trace[:-1] + trace.nodes[i + 1 :]),
378-
f"Subtraction (Removed {s_trace[i]} from position {i}): "
379-
+ "->".join(s_trace[:-1] + trace.nodes[i + 1 :]),
380-
]
381-
)
382-
return potential_subtraces
383-
384-
def determine_shapley_value(self, log, constraints, index):
385-
"""Determines the Shapley value-based contribution of a constraint to a the
386-
overall conformance rate.
387-
Args:
388-
log (dictionary): The event log, where keys are strings and values are
389-
ints
390-
constraints (list): A list of constraints (regexp strings)
391-
index (int): The
392-
Returns:
393-
float: The contribution of the constraint to the overall conformance
394-
rate
395-
"""
396-
if len(constraints) < index:
397-
raise Exception("Constraint not in constraint list.")
398-
contributor = constraints[index]
399-
sub_ctrbs = []
400-
reduced_constraints = [c for c in constraints if not c == contributor]
401-
subsets = determine_powerset(reduced_constraints)
402-
for subset in subsets:
403-
lsubset = list(subset)
404-
constraints_without = [c for c in constraints if c in lsubset]
405-
constraints_with = [c for c in constraints if c in lsubset + [contributor]]
406-
weight = (
407-
math.factorial(len(lsubset))
408-
* math.factorial(len(constraints) - 1 - len(lsubset))
409-
) / math.factorial(len(constraints))
410-
sub_ctrb = weight * (
411-
self.determine_conformance_rate(log, constraints_without)
412-
- self.determine_conformance_rate(log, constraints_with)
413-
)
414-
sub_ctrbs.append(sub_ctrb)
415-
return sum(sub_ctrbs)
416-
417-
def evaluate_similarity(self, trace):
418-
"""
419-
Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance.
420-
421-
:param trace: The trace to compare with the adherent trace.
422-
:return: A normalized score indicating the similarity between the adherent trace and the given trace.
423-
"""
424-
length = len(self.adherent_trace)
425-
trace_len = len("".join(trace))
426-
lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace))
427-
max_distance = max(length, trace_len)
428-
normalized_score = 1 - lev_distance / max_distance
429-
return normalized_score
430-
431-
def determine_conformance_rate(self, event_log, constraints=None):
432-
"""
433-
Determines the conformance rate of the event log based on the given constraints.
434-
435-
:param event_log: The event log to analyze.
436-
:param constraints: The constraints to check against the event log.
437-
:return: The conformance rate as a float between 0 and 1, or a message if no constraints are provided.
438-
"""
439-
if not self.constraints and not constraints:
440-
return "The explainer have no constraints"
441-
len_log = len(event_log)
442-
if len_log == 0:
443-
return 1
444-
non_conformant = 0
445-
if constraints == None:
446-
constraints = self.constraints
447-
for trace, count in event_log.log.items():
448-
for con in constraints:
449-
if not re.search(con, "".join(trace)):
450-
non_conformant += count
451-
break
452-
return (len_log - non_conformant) / len_log
453-
454-
def trace_contribution_to_conformance_loss(
455-
self, event_log, trace, constraints=None
456-
):
457-
"""
458-
Calculates the contribution of a specific trace to the conformance loss of the event log.
459-
460-
:param event_log: The event log to analyze.
461-
:param trace: The trace to calculate its contribution.
462-
:param constraints: The constraints to check against the event log.
463-
:return: The contribution of the trace to the conformance loss as a float between 0 and 1.
464-
"""
465-
if not constraints:
466-
constraints = self.constraints
467-
total_traces = len(event_log)
468-
contribution_of_trace = 0
469-
for t, count in event_log.log.items():
470-
if not self.conformant(t, constraints):
471-
if trace.nodes == list(t):
472-
contribution_of_trace = count
473-
474-
return contribution_of_trace / total_traces
475-
476-
477108
def determine_powerset(elements):
478109
"""Determines the powerset of a list of elements
479110
Args:
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Please sign in to comment.