From e61a1580d41ea9fba1e17eed9c602984d6a214e3 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 21 Feb 2024 14:28:40 +0100 Subject: [PATCH 01/54] Initial commit explainer module --- .gitignore | 3 + explainer/README.md | 47 ++++ explainer/explainer.py | 294 ++++++++++++++++++++ explainer/tutorial/explainer_tutorial.ipynb | 184 ++++++++++++ tests/explainer/explainer_test.py | 161 +++++++++++ 5 files changed, 689 insertions(+) create mode 100644 explainer/README.md create mode 100644 explainer/explainer.py create mode 100644 explainer/tutorial/explainer_tutorial.ipynb create mode 100644 tests/explainer/explainer_test.py diff --git a/.gitignore b/.gitignore index 3dc4d04..8a44e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,6 @@ dmypy.json .idea/ .vscode/ + +# explainer Stuff +explainer/test.py \ No newline at end of file diff --git a/explainer/README.md b/explainer/README.md new file mode 100644 index 0000000..e4052a2 --- /dev/null +++ b/explainer/README.md @@ -0,0 +1,47 @@ +# Symbolic Explanations of Process Conformance Violations +## Introduction + + +# Regex usage for the first iteration of the software + +## 1. Sequence Constraint +Pattern: `'A.*B.*C'` + +Explanation: This regex specifies that for a trace to be conformant, it must contain the nodes 'A', 'B', and 'C' in that order, though not necessarily consecutively. The .* allows for any number of intervening nodes between the specified nodes. + +> Example: A trace ['A', 'X', 'B', 'Y', 'C'] would be conformant, while ['A', 'C', 'B'] would not. + +## 2. Immediate Succession +Pattern: `'AB'` + +Explanation: This regex specifies that node 'A' must be immediately followed by node 'B' with no intervening nodes. + +> Example: A trace ['A', 'B', 'C'] would be conformant, while ['A', 'X', 'B'] would not. + +## 3. Optional Node +Pattern: `'A(B)?C'` + +Explanation: This regex specifies that the node 'B' is optional between 'A' and 'C'. The node 'C' must follow 'A', but 'B' can either be present or absent. + +> Example: Both traces ['A', 'B', 'C'] and ['A', 'C'] would be conformant. + +## 4. Excluding Specific Nodes +Pattern: `'A[^D]*B'` + +Explanation: This regex specifies that 'A' must be followed by 'B' without the occurrence of 'D' in between them. The [^D] part matches any character except 'D'. + +> Example: A trace ['A', 'C', 'B'] would be conformant, while ['A', 'D', 'B'] would not. + +## 5. Repetition of Nodes +Pattern: `'A(B{2,3})C'` + +Explanation: This regex specifies that 'A' must be followed by 'B' repeated 2 to 3 times and then followed by 'C'. + +> Example: Traces ['A', 'B', 'B', 'C'] and ['A', 'B', 'B', 'B', 'C'] would be conformant, while ['A', 'B', 'C'] or ['A', 'B', 'B', 'B', 'B', 'C'] would not. + +## 6. Alternative Paths +Pattern: `'A(B|D)C'` + +Explanation: This regex specifies that after 'A', there must be either a 'B' or a 'D', followed by a 'C'. + +> Example: Both traces ['A', 'B', 'C'] and ['A', 'D', 'C'] would be conformant. diff --git a/explainer/explainer.py b/explainer/explainer.py new file mode 100644 index 0000000..96781fb --- /dev/null +++ b/explainer/explainer.py @@ -0,0 +1,294 @@ +import itertools +import re +from itertools import combinations + +class Trace: + def __init__(self, nodes): + """ + Initializes a Trace instance. + + :param nodes: A list of nodes where each node is represented as a string label. + """ + self.nodes = nodes + def __len__(self): + return len(self.nodes) + def __iter__(self): + self.index = 0 + return self + + def __next__(self): + if self.index < len(self.nodes): + result = self.nodes[self.index] + self.index += 1 + return result + else: + raise StopIteration + +class Explainer: + def __init__(self): + """ + Initializes an Explainer instance. + """ + self.constraints = [] # List to store constraints (regex patterns) + self.nodes = set() # Set to store unique nodes involved in constraints + + def add_constraint(self, regex): + """ + Adds a new constraint and updates the nodes list. + + :param regex: A regular expression representing the constraint. + """ + self.constraints.append(regex) + # Extract unique characters (nodes) from the regex and update the nodes set + unique_nodes = set(filter(str.isalpha, regex)) + self.nodes.update(unique_nodes) + + def remove_constraint(self, idx): + """ + Removes a constraint by index and updates the nodes list if necessary. + + :param idx: Index of the constraint to be removed. + """ + if 0 <= idx < len(self.constraints): + removed_regex = self.constraints.pop(idx) + removed_nodes = set(filter(str.isalpha, removed_regex)) + + # Re-evaluate nodes to keep based on remaining constraints + remaining_nodes = set(filter(str.isalpha, ''.join(self.constraints))) + self.nodes = remaining_nodes + + # Optionally, remove nodes that are no longer in any constraint + for node in removed_nodes: + if node not in remaining_nodes: + self.nodes.discard(node) + + def activation(self, trace): + """ + Checks if any of the nodes in the trace activates any constraint. + + :param trace: A Trace instance. + :return: Boolean indicating if any constraint is activated. + """ + trace_str = ''.join(trace.nodes) + return any(re.search(constraint, trace_str) for constraint in self.constraints) + + def conformant(self, trace): + """ + Checks if the trace is conformant according to all the constraints. + + :param trace: A Trace instance. + :return: Boolean indicating if the trace is conformant with all constraints. + """ + trace_str = ''.join(trace) + return all(re.search(constraint, trace_str) for constraint in self.constraints) + + + def minimal_expl(self, trace): + """ + Provides a minimal explanation for non-conformance, given the trace and constraints. + + :param trace: A Trace instance. + :return: Explanation of why the trace is non-conformant. + """ + if self.conformant(trace): + return "The trace is already conformant, no changes needed." + + explanations = None + + for constraint in self.constraints: + for subtrace in get_sublists(trace): + trace_str = ''.join(subtrace) + if not re.search(constraint, trace_str): + explanations = f"Constraint ({constraint}) is violated by subtrace: {subtrace}" + break + + if explanations: + return "Non-conformance due to: " + explanations + else: + return "Trace is non-conformant, but the specific constraint violation could not be determined." + + def counterfactual_expl(self, trace): + """ + Provides a counterfactual explanation for a non-conformant trace, suggesting changes to adhere to the constraints. + + :param trace: A Trace instance. + :return: Suggestion to make the trace conformant. + """ + if self.conformant(trace): + return "The trace is already conformant, no changes needed." + + violated_constraints = self.identify_violated_constraints(trace) + if not violated_constraints: + return "Unable to identify specific violated constraints." + + counterfactuals = [] + for constraint in violated_constraints: + counterfactuals.extend(self.generate_potential_counterfactuals(trace, constraint)) + + conformant_counterfactuals = [cf for cf in counterfactuals if self.conformant(cf[0])] + if conformant_counterfactuals: + selected_counterfactual = min(conformant_counterfactuals, key=lambda x: len(x[0].nodes)) # Example selection criteria + return f"Suggested change to make the trace ({trace.nodes}) conformant: {selected_counterfactual[1]}" + else: + return "Unable to generate potential counterfactuals." + + + def identify_violated_constraints(self, trace): + """ + Identifies which constraints are violated by the given trace. + + :param trace: A Trace instance to check against the constraints. + :return: A list of constraints that the trace violates. + """ + violated = [] + trace_str = ''.join(trace.nodes) + for constraint in self.constraints: + if not re.search(constraint, trace_str): + violated.append(constraint) + return violated + + def introduces_new_violations(self, counterfactual, violated_constraints): + """ + Checks if a counterfactual trace introduces new violations. + + :param counterfactual: A modified Trace instance to check. + :param violated_constraints: Constraints that were initially violated. + :return: True if new violations are introduced, False otherwise. + """ + for constraint in self.constraints: + if constraint not in violated_constraints and not re.search(constraint, ''.join(counterfactual.nodes)): + return True + return False + + def generate_potential_counterfactuals(self, trace, violated_constraint): + """ + Generates potential counterfactual modifications for a trace based on a violated constraint. + + :param trace: The original Trace instance. + :param violated_constraint: The specific constraint that is violated. + :return: A list of counterfactuals suggesting how to modify the trace. + """ + trace_str = "".join(trace) + if re.search(violated_constraint, trace_str): + return f"Trace: {trace_str} is conformant for the constraint: {violated_constraint}" + # Extrace all the nodes in the constraint + used_nodes = self.get_nodes_from_constraint(violated_constraint) + # Extract which part of the trace that violates the constraint + violating_subtraces = self.get_violating_subtrace(trace, violated_constraint) + # Generate counterfactuals + addition_counterfactuals = self.addition_modification(trace, used_nodes, violating_subtraces) + subtraction_counterfactuals = self.subtraction_modification(trace) + reordering_counterfactuals = self.reordering_modification(trace) + substitution_counterfactuals = self.substitution_modification(trace, used_nodes) + + return addition_counterfactuals + subtraction_counterfactuals + reordering_counterfactuals + substitution_counterfactuals + + + def get_nodes_from_constraint(self, constraint): + """ + Extracts unique nodes from a constraint pattern. + + :param constraint: The constraint pattern as a string. + :return: A list of unique nodes found within the constraint. + """ + return list(set(re.findall(r'[A-Za-z]', constraint))) + + def select_best_counterfactual(self, counter_factuals): + """ + Selects the best counterfactual modification from a list. + + :param counter_factuals: A list of counterfactual modifications. + :return: The selected best counterfactual modification. + TODO: Implement this based on a heuristic + """ + return counter_factuals[0] + + def get_violating_subtrace(self, trace, constraint): + """ + Finds subtraces of a given trace that violate a specific constraint. + + :param trace: The Trace instance to analyze. + :param constraint: The constraint to check against the trace. + :return: A list of subtraces that violate the given constraint. + """ + violating_subtrace = [] + for subtrace in get_sublists(trace): + trace_str = ''.join(subtrace) + if not re.search(constraint, trace_str): + violating_subtrace.append(subtrace) + return violating_subtrace + + def addition_modification(self, trace, used_nodes, violating_subtraces): + """ + Suggests additions to the trace to meet constraints. + """ + counterfactuals = [] + for subtrace in violating_subtraces: + for i in range(len(subtrace) - 1): + for node in used_nodes: + new_trace = list(trace.nodes) # Ensure we're working with a full trace copy + new_trace.insert(i + 1, node) # Insert a node between each node in the subtrace + new_trace_str = "Addition: " + "->".join(new_trace) + counterfactuals.append((Trace(new_trace), new_trace_str)) + return counterfactuals + + + def subtraction_modification(self, trace): + """ + Suggests node removals from the trace for conformance. + """ + counterfactuals = [] + + for r in range(1, len(trace.nodes)): + for indices_to_remove in combinations(range(len(trace.nodes)), r): + modified_trace_nodes = [node for index, node in enumerate(trace.nodes) if index not in indices_to_remove] + + removed_nodes_str = "->".join([trace.nodes[index] for index in indices_to_remove]) + new_trace_str = f"Subtraction (Removed {removed_nodes_str}): " + "->".join(modified_trace_nodes) + + counterfactuals.append((Trace(modified_trace_nodes), new_trace_str)) + return counterfactuals + + + + def reordering_modification(self, trace): + """ + Suggests reordering of nodes in the trace for conformance. + """ + counterfactuals = [] + permutations = itertools.permutations(trace.nodes) + for perm in permutations: + if perm not in [cf[0].nodes for cf in counterfactuals]: + new_trace_str = "Reordering: " + "->".join(perm) # Descriptive string + counterfactuals.append((Trace(list(perm)), new_trace_str)) + return counterfactuals + + + def substitution_modification(self, trace, used_nodes): + """ + Suggests substitutions within the trace to meet constraints. + """ + counterfactuals = [] + for i, node in enumerate(trace.nodes): + if node in used_nodes: + for replacement_node in (self.nodes - set([node])): # Ensure it's a set operation + new_trace_nodes = trace.nodes[:] # Copy the list of nodes + new_trace_nodes[i] = replacement_node + new_trace_str = f"Substitution: Replace {node} with {replacement_node} at position {i+1}" + new_trace = Trace(new_trace_nodes) + if new_trace not in [cf[0] for cf in counterfactuals]: + counterfactuals.append((new_trace, new_trace_str)) + return counterfactuals + + +def get_sublists(lst): + """ + Generates all possible non-empty sublists of a list. + + :param lst: The input list. + :return: A list of all non-empty sublists. + """ + sublists = [] + for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n + sublists.extend(combinations(lst, r)) + return sublists diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb new file mode 100644 index 0000000..33d4c57 --- /dev/null +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Explainer utility in BPMN2CONSTRAINTS\n", + "\n", + "In this notebook, we explore the `Explainer` class, designed to analyze and explain the conformance of traces against predefined constraints. Trace analysis is crucial in domains such as process mining, where understanding the behavior of system executions against expected models can uncover inefficiencies, deviations, or compliance issues.\n", + "\n", + "The constraints currently consists of basic regex, this is because of it's similiarities and likeness to declarative constraints used in BPMN2CONSTRAINTS\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('../')\n", + "from explainer import Explainer, Trace" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Basic Usage\n", + "Let's start by creating an instance of the `Explainer` and adding a simple constraint that a valid trace should contain the sequence \"A\" followed by \"B\" and then \"C\".\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "explainer = Explainer()\n", + "explainer.add_constraint('A.*B.*C')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Analyzing Trace Conformance\n", + "\n", + "Now, we'll create a trace and check if it conforms to the constraints we've defined." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Is the trace conformant? True\n" + ] + } + ], + "source": [ + "trace = Trace(['A', 'X', 'B', 'Y', 'C'])\n", + "is_conformant = explainer.conformant(trace)\n", + "print(f\"Is the trace conformant? {is_conformant}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Explaining Non-conformance\n", + "\n", + "If a trace is not conformant, we can use the `minimal_expl` and `counterfactual_expl` methods to understand why and how to adjust the trace.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraint: A.*B.*C\n", + "Trace:['A', 'C']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", + "-----------\n", + "Constraint: A.*B.*C\n", + "Trace:['A', 'C']\n", + "Suggested change to make the trace (['A', 'C']) conformant: Addition: A->B->C\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", + "-----------\n", + "Constraint: A.*B.*C\n", + "Trace:['C', 'B', 'A']\n", + "Suggested change to make the trace (['C', 'B', 'A']) conformant: Reordering: A->B->C\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", + "-----------\n", + "Constraint: A.*B.*C\n", + "Trace:['A', 'A', 'C']\n", + "Suggested change to make the trace (['A', 'A', 'C']) conformant: Substitution: Replace A with B at position 2\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", + "-----------\n", + "Constraint: AC\n", + "Trace:['A', 'X', 'C']\n", + "Suggested change to make the trace (['A', 'X', 'C']) conformant: Subtraction (Removed X): A->C\n", + "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n" + ] + } + ], + "source": [ + "non_conformant_trace = Trace(['A', 'C'])\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.counterfactual_expl(non_conformant_trace)) # Addition\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "\n", + "non_conformant_trace = Trace(['C', 'B', 'A']) #Reordering\n", + "print('-----------')\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "\n", + "non_conformant_trace = Trace(['A','A','C']) #Substitution\n", + "print('-----------')\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "\n", + "explainer.remove_constraint(0)\n", + "explainer.add_constraint('AC')\n", + "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", + "print('-----------')\n", + "print('Constraint: AC')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: Coming soon" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py new file mode 100644 index 0000000..b480532 --- /dev/null +++ b/tests/explainer/explainer_test.py @@ -0,0 +1,161 @@ +from explainer.explainer import * + +# Test 1: Adding and checking constraints +def test_add_constraint(): + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + assert 'A.*B.*C' in explainer.constraints, "Constraint 'A.*B.*C' should be added." + +# Test 2: Removing constraints +def test_remove_constraint(): + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + explainer.add_constraint('B.*C') + explainer.remove_constraint(0) + assert 'A.*B.*C' not in explainer.constraints, "Constraint 'A.*B.*C' should be removed." + +# Test 3: Activation of constraints +def test_activation(): + trace = Trace(['A', 'B', 'C']) + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + assert explainer.activation(trace), "The trace should activate the constraint." + +# Test 4: Checking conformance of traces +def test_conformance(): + trace = Trace(['A', 'B', 'C']) + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + assert explainer.conformant(trace), "The trace should be conformant." + +# Test 5: Non-conformance explanation +def test_non_conformance_explanation(): + trace = Trace(['C', 'A', 'B']) + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + explanation = explainer.minimal_expl(trace) + assert "violated" in explanation, "The explanation should indicate a violation." + +# Test 6: Overlapping constraints +def test_overlapping_constraints(): + trace = Trace(['A', 'B', 'A', 'C']) + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + explainer.add_constraint('A.*A.*C') + assert explainer.conformant(trace), "The trace should be conformant with overlapping constraints." + +# Test 7: Partially meeting constraints +def test_partial_conformance(): + trace = Trace(['A', 'C', 'B']) + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + assert not explainer.conformant(trace), "The trace should not be fully conformant." + +# Test 8: Constraints with repeated nodes +def test_constraints_with_repeated_nodes(): + trace = Trace(['A', 'A', 'B', 'A']) + explainer = Explainer() + explainer.add_constraint('A.*A.*B.*A') + assert explainer.conformant(trace), "The trace should conform to the constraint with repeated nodes." + +# Test 9: Removing constraints and checking nodes list +def test_remove_constraint_and_check_nodes(): + explainer = Explainer() + explainer.add_constraint('A.*B') + explainer.add_constraint('B.*C') + explainer.remove_constraint(0) + assert 'A' not in explainer.nodes and 'B' in explainer.nodes and 'C' in explainer.nodes, "Node 'A' should be removed, while 'B' and 'C' remain." + +# Test 10: Complex regex constraint +def test_complex_regex_constraint(): + trace = Trace(['A', 'X', 'B', 'Y', 'C']) + explainer = Explainer() + explainer.add_constraint('A.*X.*B.*Y.*C') # Specifically expects certain nodes in order + assert explainer.conformant(trace), "The trace should conform to the complex regex constraint." + +# Test 11: Constraint not covered by any trace node +def test_constraint_not_covered(): + trace = Trace(['A', 'B', 'C']) + explainer = Explainer() + explainer.add_constraint('D') # This node 'D' does not exist in the trace + assert not explainer.activation(trace), "The constraint should not be activated by the trace." + +# Test 12: Empty trace and constraints +def test_empty_trace_and_constraints(): + trace = Trace([]) + explainer = Explainer() + explainer.add_constraint('') # Adding an empty constraint + assert explainer.conformant(trace), "An empty trace should be conformant with an empty constraint." + +# Test 13: Removing non-existent constraint index +def test_remove_nonexistent_constraint(): + explainer = Explainer() + explainer.add_constraint('A.*B') + explainer.remove_constraint(10) # Non-existent index + assert len(explainer.constraints) == 1, "Removing a non-existent constraint should not change the constraints list." + +# Test 14: Activation with no constraints +def test_activation_with_no_constraints(): + trace = Trace(['A', 'B', 'C']) + explainer = Explainer() + assert not explainer.activation(trace), "No constraints should mean no activation." + +# Test 15: Trace conformance against multiple constraints +def test_trace_conformance_against_multiple_constraints(): + trace1 = Trace(['A', 'B', 'D']) # This trace should not be fully conformant as it only matches one constraint + trace2 = Trace(['A', 'B', 'C', 'D']) # This trace should be conformant as it matches both constraints + + explainer = Explainer() + explainer.add_constraint('A.*B.*C') # Both traces attempt to conform to this + explainer.add_constraint('B.*D') # And to this + + # Checking conformance + assert not explainer.conformant(trace1), "Trace1 should not be conformant as it does not satisfy all constraints." + assert explainer.conformant(trace2), "Trace2 should be conformant as it satisfies all constraints." + +# Test 16: Conformant trace does not generate minimal explaination +def test_conformant_trace_handled_correctly(): + trace = Trace(['A', 'B']) + explainer = Explainer() + explainer.add_constraint('AB') + print(explainer.minimal_expl(trace)) + assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed." +# Test 17: Conformant trace +def test_explainer_methods(): + trace = Trace(['A', 'B', 'C']) + explainer = Explainer() + explainer.add_constraint('A.*B.*C') + explainer.add_constraint('B.*C') + + + assert explainer.conformant(trace) == True, "Test 1 Failed: Trace should be conformant." + assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed.", "Test 1 Failed: Incorrect minimal explanation for a conformant trace." + assert explainer.counterfactual_expl(trace) == "The trace is already conformant, no changes needed.", "Test 1 Failed: Incorrect counterfactual explanation for a conformant trace." +# Test 18: Some explaination test +def test_explaination(): + explainer = Explainer() + + conformant_trace = Trace(['A','B','C']) + non_conformant_trace = Trace(['A','C']) + + explainer.add_constraint('A.*B.*C') + + assert explainer.conformant(non_conformant_trace) == False + assert explainer.conformant(conformant_trace) == True + assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')" + assert explainer.counterfactual_expl(non_conformant_trace) == "Suggested change to make the trace (['A', 'C']) conformant: Addition: A->B->C" +# Test 19: Complex explaination test. +""" +This part is not very complex as of now and is very much up for change, the complexity of counterfactuals +proved to be slightly larger than expected +""" +def test_complex_counterfactual_explanation(): + explainer = Explainer() + + explainer.add_constraint('ABB*C') + + non_conformant_trace = Trace(['A', 'C', 'E', 'D']) + + counterfactual_explanation = explainer.counterfactual_expl(non_conformant_trace) + + assert counterfactual_explanation == "Suggested change to make the trace (['A', 'C', 'E', 'D']) conformant: Addition: A->B->C->E->D" \ No newline at end of file From 3ace9dbb8d4cd238590d853b63903fde50ddd69c Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 28 Feb 2024 00:35:41 +0100 Subject: [PATCH 02/54] Some experimentation --- explainer/explainer.py | 345 ++++++++++++-------- explainer/tutorial/explainer_tutorial.ipynb | 12 +- tutorial/tutorial.ipynb | 4 +- 3 files changed, 223 insertions(+), 138 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 96781fb..76a6350 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -23,6 +23,11 @@ def __next__(self): return result else: raise StopIteration + def __split__(self): + spl = [] + for node in self.nodes: + spl.append(node) + return spl class Explainer: def __init__(self): @@ -31,7 +36,16 @@ def __init__(self): """ self.constraints = [] # List to store constraints (regex patterns) self.nodes = set() # Set to store unique nodes involved in constraints + self.constraint_fulfillment_alpha = 1 + self.repetition_alpha = 1 + self.sub_trace_adherence_alpha = 1 + def set_heuristic_alpha(self, constraint_fulfillment_alpha = 1, repetition_alpha = 1, sub_trace_adherence_alpha = 1): + self.constraint_fulfillment_alpha = constraint_fulfillment_alpha + self.repetition_alpha = repetition_alpha + self.sub_trace_adherence_alpha + return + def add_constraint(self, regex): """ Adds a new constraint and updates the nodes list. @@ -109,130 +123,126 @@ def minimal_expl(self, trace): def counterfactual_expl(self, trace): """ - Provides a counterfactual explanation for a non-conformant trace, suggesting changes to adhere to the constraints. - - :param trace: A Trace instance. - :return: Suggestion to make the trace conformant. + 3 Heuristics: + Constraint fulfillment - Prioritize modifications that maximize the number of constraints the trace fulfills. + Minimal Deviation - Seek the least number of changes necessary to make the trace adhere to constraints. + Sub-trace Adherence - Evaluate the proportion of the trace that adheres to constraints after each modification. """ if self.conformant(trace): return "The trace is already conformant, no changes needed." + # Evaluate heuristic for original trace + constraint_fulfillment_score = 1 + if len(self.constraints) > 1: + constraint_fulfillment_score = self.evaluate(trace, "constraint_fulfillment") + sub_trace_adherence_score = self.evaluate(trace, "sub_trace_adherence") + repetition_score = self.evaluate(trace, "repetition") + # Identify the lowest score and the corresponding heuristic + scores = { + 'constraint_fulfillment': constraint_fulfillment_score, + 'sub_trace_adherence': sub_trace_adherence_score, + 'repetition' : repetition_score + } + + lowest_heuristic, lowest_score = min(scores.items(), key=lambda x: x[1]) - violated_constraints = self.identify_violated_constraints(trace) - if not violated_constraints: - return "Unable to identify specific violated constraints." - - counterfactuals = [] - for constraint in violated_constraints: - counterfactuals.extend(self.generate_potential_counterfactuals(trace, constraint)) - - conformant_counterfactuals = [cf for cf in counterfactuals if self.conformant(cf[0])] - if conformant_counterfactuals: - selected_counterfactual = min(conformant_counterfactuals, key=lambda x: len(x[0].nodes)) # Example selection criteria - return f"Suggested change to make the trace ({trace.nodes}) conformant: {selected_counterfactual[1]}" + # Perform operation based on the lowest scoring heuristic + if lowest_heuristic: + return self.operate_on_trace(trace, lowest_heuristic, lowest_score, "") else: - return "Unable to generate potential counterfactuals." - - - def identify_violated_constraints(self, trace): - """ - Identifies which constraints are violated by the given trace. + return "Error identifying the lowest scoring heuristic." - :param trace: A Trace instance to check against the constraints. - :return: A list of constraints that the trace violates. - """ - violated = [] - trace_str = ''.join(trace.nodes) - for constraint in self.constraints: - if not re.search(constraint, trace_str): - violated.append(constraint) - return violated - - def introduces_new_violations(self, counterfactual, violated_constraints): - """ - Checks if a counterfactual trace introduces new violations. + def counter_factual_helper(self, working_trace, explanation, depth = 0): + if self.conformant(working_trace): + print(depth) + return explanation + if depth > 100: + return f'{explanation}\n Maximum depth of {depth -1} reached' + # Evaluate heuristic for original trace + constraint_fulfillment_score = 1 + if len(self.constraints) > 1: + constraint_fulfillment_score = self.evaluate(working_trace, "constraint_fulfillment") + sub_trace_adherence_score = self.evaluate(working_trace, "sub_trace_adherence") + repetition_score = self.evaluate(working_trace, "repetition") + if constraint_fulfillment_score == 0 and sub_trace_adherence_score == 0: + self.constraint_fulfillment_alpha = 1 + self.sub_trace_adherence_alpha = 1 + return self.counter_factual_helper(working_trace, explanation, depth + 1) + # Identify the lowest score and the corresponding heuristic + scores = { + 'sub_trace_adherence': sub_trace_adherence_score, + 'repetition' : repetition_score, + 'constraint_fulfillment': constraint_fulfillment_score, + } - :param counterfactual: A modified Trace instance to check. - :param violated_constraints: Constraints that were initially violated. - :return: True if new violations are introduced, False otherwise. - """ - for constraint in self.constraints: - if constraint not in violated_constraints and not re.search(constraint, ''.join(counterfactual.nodes)): - return True - return False - - def generate_potential_counterfactuals(self, trace, violated_constraint): - """ - Generates potential counterfactual modifications for a trace based on a violated constraint. - - :param trace: The original Trace instance. - :param violated_constraint: The specific constraint that is violated. - :return: A list of counterfactuals suggesting how to modify the trace. - """ - trace_str = "".join(trace) - if re.search(violated_constraint, trace_str): - return f"Trace: {trace_str} is conformant for the constraint: {violated_constraint}" - # Extrace all the nodes in the constraint - used_nodes = self.get_nodes_from_constraint(violated_constraint) - # Extract which part of the trace that violates the constraint - violating_subtraces = self.get_violating_subtrace(trace, violated_constraint) - # Generate counterfactuals - addition_counterfactuals = self.addition_modification(trace, used_nodes, violating_subtraces) - subtraction_counterfactuals = self.subtraction_modification(trace) - reordering_counterfactuals = self.reordering_modification(trace) - substitution_counterfactuals = self.substitution_modification(trace, used_nodes) - - return addition_counterfactuals + subtraction_counterfactuals + reordering_counterfactuals + substitution_counterfactuals + lowest_heuristic, lowest_score = min(scores.items(), key=lambda x: x[1]) + # Perform operation based on the lowest scoring heuristic + if lowest_heuristic: + return self.operate_on_trace(working_trace, lowest_heuristic, lowest_score, explanation, depth) + else: + return "Error identifying the lowest scoring heuristic." - - def get_nodes_from_constraint(self, constraint): + def operate_on_trace(self, trace, heuristic, score, explanation_path, depth = 0): + explanation = None + counter_factuals = self.modify_subtrace(trace) + best_subtrace = None + best_score = -float('inf') + for subtrace in counter_factuals: + current_score = self.evaluate(subtrace[0], heuristic) + if current_score > best_score and current_score > score: + best_score = current_score + best_subtrace = subtrace[0] + explanation = subtrace[1] + if best_subtrace == None: + for subtrace in counter_factuals: + print(subtrace[0].nodes) + print(heuristic) + self.operate_on_trace(subtrace[0], heuristic, score, explanation_path, depth + 1) + explanation_string = explanation_path + '\n' + str(explanation) + f", based on heurstic {heuristic}" + return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) + + def get_nodes_from_constraint(self, constraint = None): """ Extracts unique nodes from a constraint pattern. :param constraint: The constraint pattern as a string. :return: A list of unique nodes found within the constraint. """ - return list(set(re.findall(r'[A-Za-z]', constraint))) - - def select_best_counterfactual(self, counter_factuals): - """ - Selects the best counterfactual modification from a list. - - :param counter_factuals: A list of counterfactual modifications. - :return: The selected best counterfactual modification. - TODO: Implement this based on a heuristic - """ - return counter_factuals[0] + if constraint is None: + all_nodes = set() + for constr in self.constraints: + all_nodes.update(re.findall(r'[A-Za-z]+', constr)) + return list(set(all_nodes)) + else: + return list(set(re.findall(r'[A-Za-z]', constraint))) - def get_violating_subtrace(self, trace, constraint): - """ - Finds subtraces of a given trace that violate a specific constraint. + def modify_subtrace(self, trace): - :param trace: The Trace instance to analyze. - :param constraint: The constraint to check against the trace. - :return: A list of subtraces that violate the given constraint. - """ - violating_subtrace = [] - for subtrace in get_sublists(trace): - trace_str = ''.join(subtrace) - if not re.search(constraint, trace_str): - violating_subtrace.append(subtrace) - return violating_subtrace + add_mod = self.addition_modification(trace) + sub_mod = self.subtraction_modification(trace) + + return sub_mod + add_mod - def addition_modification(self, trace, used_nodes, violating_subtraces): + from itertools import combinations, chain + + def addition_modification(self, trace): """ - Suggests additions to the trace to meet constraints. + Suggests additions to the trace to meet constraints, but only one node at a time. """ counterfactuals = [] - for subtrace in violating_subtraces: - for i in range(len(subtrace) - 1): - for node in used_nodes: - new_trace = list(trace.nodes) # Ensure we're working with a full trace copy - new_trace.insert(i + 1, node) # Insert a node between each node in the subtrace - new_trace_str = "Addition: " + "->".join(new_trace) - counterfactuals.append((Trace(new_trace), new_trace_str)) - return counterfactuals + possible_additions = self.get_nodes_from_constraint() + # Only add one node at a time + for added_node in possible_additions: + for insertion_point in range(len(trace.nodes) + 1): + # Create a new trace with the added node + new_trace_nodes = trace.nodes[:insertion_point] + [added_node] + trace.nodes[insertion_point:] + new_trace_str = f"Addition (Added {added_node} at position {insertion_point}): " + "->".join(new_trace_nodes) + + counterfactuals.append((Trace(new_trace_nodes), new_trace_str)) + + return counterfactuals + def subtraction_modification(self, trace): """ Suggests node removals from the trace for conformance. @@ -249,38 +259,67 @@ def subtraction_modification(self, trace): counterfactuals.append((Trace(modified_trace_nodes), new_trace_str)) return counterfactuals + def evaluate(self, trace, heurstic): + if heurstic == "constraint_fulfillment": + return self.evaluate_constraint_fulfillment(trace) + elif heurstic == "sub_trace_adherence": + return self.evaluate_sub_trace_adherence(trace) + elif heurstic == "repetition": + return self.evaluate_repetition(trace) + else: + return "No valid evaluation method" + + def evaluate_constraint_fulfillment(self, optional_trace): + if self.constraint_fulfillment_alpha == 0: + return 0 + fulfilled_constraints = sum(1 for constraint in self.constraints if re.search(constraint,"".join(optional_trace))) + total_constraints = len(self.constraints) + return (fulfilled_constraints / total_constraints) * self.constraint_fulfillment_alpha if total_constraints else 0 + def evaluate_repetition(self, trace): + if self.repetition_alpha == 0: + return 1 - def reordering_modification(self, trace): - """ - Suggests reordering of nodes in the trace for conformance. - """ - counterfactuals = [] - permutations = itertools.permutations(trace.nodes) - for perm in permutations: - if perm not in [cf[0].nodes for cf in counterfactuals]: - new_trace_str = "Reordering: " + "->".join(perm) # Descriptive string - counterfactuals.append((Trace(list(perm)), new_trace_str)) - return counterfactuals + node_counts = {} + for node in trace.nodes: + if node in node_counts: + node_counts[node] += 1 + else: + node_counts[node] = 1 + # Calculate the deviation of each node's occurrence from 1 + deviations = [count - 1 for count in node_counts.values()] + + # Normalize deviation: Here, we take the sum of deviations and divide by the total number of nodes + # This gives an average deviation per node, which we normalize by dividing by the length of the trace + # This assumes the worst case where every node in the trace is different and repeated once + if trace.nodes: + normalized_deviation = sum(deviations) / len(trace.nodes) + else: + normalized_deviation = 0 + + # Ensure the score is between 0 and 1 + normalized_deviation = 1 - min(max(normalized_deviation, 0), 1) + + return normalized_deviation * self.repetition_alpha - def substitution_modification(self, trace, used_nodes): - """ - Suggests substitutions within the trace to meet constraints. - """ - counterfactuals = [] - for i, node in enumerate(trace.nodes): - if node in used_nodes: - for replacement_node in (self.nodes - set([node])): # Ensure it's a set operation - new_trace_nodes = trace.nodes[:] # Copy the list of nodes - new_trace_nodes[i] = replacement_node - new_trace_str = f"Substitution: Replace {node} with {replacement_node} at position {i+1}" - new_trace = Trace(new_trace_nodes) - if new_trace not in [cf[0] for cf in counterfactuals]: - counterfactuals.append((new_trace, new_trace_str)) - return counterfactuals + def evaluate_sub_trace_adherence(self, optional_trace): + sub_lists = list(set([node for node in optional_trace])) + adherence_scores = [[0 for _ in self.constraints] for _ in sub_lists] + for i, sub_trace in enumerate(sub_lists): + trace_string = "".join(sub_trace) + for j, con in enumerate(self.constraints): + match = re.search(trace_string, con) + if match: + adherence_scores[i][j] = 1 + num_nodes = len(self.get_nodes_from_constraint()) + total_scores = sum(sum(row) for row in adherence_scores) + + average_score = total_scores / num_nodes if num_nodes else 0 + return average_score * self.sub_trace_adherence_alpha + def get_sublists(lst): """ Generates all possible non-empty sublists of a list. @@ -292,3 +331,53 @@ def get_sublists(lst): for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n sublists.extend(combinations(lst, r)) return sublists +def get_sublists1(lst, n): + """ + Generates all possible non-empty contiguous sublists of a list, maintaining order. + + :param lst: The input list. + :return: A list of all non-empty contiguous sublists. + """ + sublists = [] + for i in range(len(lst)): + + for j in range(i + 2, min(i + n + 1, len(lst) + 1)): + sub = lst[i:j] + sublists.append(sub) + return sublists + +def levenshtein_distance(seq1, seq2): + """ + Calculates the Levenshtein distance between two sequences. + """ + size_x = len(seq1) + 1 + size_y = len(seq2) + 1 + matrix = [[0] * size_y for _ in range(size_x)] + for x in range(size_x): + matrix[x][0] = x + for y in range(size_y): + matrix[0][y] = y + + for x in range(1, size_x): + for y in range(1, size_y): + if seq1[x-1] == seq2[y-1]: + matrix[x][y] = matrix[x-1][y-1] + else: + matrix[x][y] = min( + matrix[x-1][y] + 1, # Deletion + matrix[x][y-1] + 1, # Insertion + matrix[x-1][y-1] + 1 # Substitution + ) + return matrix[size_x-1][size_y-1] + +exp = Explainer() +exp.add_constraint('A.*B.*C.*D') +#exp.add_constraint('A.*B.*C') +#exp.add_constraint('A.*B') +#optional_trace = Trace(['A', 'B', 'C', 'E', 'E']) +optional_trace = Trace(['A', 'B', 'B']) +print(exp.evaluate_repetition(optional_trace)) + + +#print(exp.counterfactual_expl(optional_trace)) + diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index 33d4c57..bc67d3a 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -39,7 +39,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -58,7 +58,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -86,17 +86,13 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Constraint: A.*B.*C\n", - "Trace:['A', 'C']\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", - "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'C']\n", "Suggested change to make the trace (['A', 'C']) conformant: Addition: A->B->C\n", diff --git a/tutorial/tutorial.ipynb b/tutorial/tutorial.ipynb index 3755a8d..7e7d8a1 100644 --- a/tutorial/tutorial.ipynb +++ b/tutorial/tutorial.ipynb @@ -106,7 +106,7 @@ "outputs": [ { "data": { - "image/png": "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94tyeLxlebiW5dl8v0CvlRBCCCFE8UIxKoQQQgghhBBCCJE0SKgQQgghhBBCCCFE0qCpH0IIkViIYzEvzn0JLPm7kkwIIYQQQhQnJFQIIURieSMH+45QcgkhhBBCiOKGpn4IIYQQQgghhBAiaZBQIYQQQgghhBBCiKRBQoUQIreUUxIoDYQQKhuEUB4VQuQ3EiqEEDnm2GOPbVu5cuXVrVu37qQ0KL5pIIRQ2SCE8qgQoiCQUCGEyHEF/8svvwy//fbbS3/yyScvnnLKKe2VBsUvDYQQKhuEUB4VQhQUEiqEEDmu4EeNGlWqe/fuZuTIkSW//fbbYQ0aNDhTaVB80kAIobJBCOVRIURBIqFCpAR///23+f3335UQCargTzrpJG8bf6nof/rpp9cPO+ywVkqD1E8DkTn2HTAXXXSRmT9/foEcf9WqVd7xv/zySyW2ygYhhPJosea6664zr732mhIihSmlJBD5yT333GNeeOGFjM/lypUz++yzjzn55JPN5ZdfbnbccccCOe8VV1xhxowZY/766y89hARV8AF8ZvuZZ545qk6dOm1mz549UWmQkDS439of1h6J8d3/rB1prZu1dFfWX2KtjbWdrM2z1s/atNBv0qx1sXaWtV2sbbD2rTVaASnbM165cqXp06ePGTt2rPn555+9bVWqVDH16tUzjLideuqpcR9r6dKlXvl39dVXm7333jvfr3XdunXe8Zs3b877qIJJZUOxo3///uaxxx7L+FypUiUvr51++umma9eupnTp0kokoTxaAEyaNMncf//9nlD+77//mp122skceeSRXtv+3HPPLZRrev31103JkiUL7fxCQkWB8PLLL5/xxRdfXDl9+vR6f/75ZwXbUC29ePHiEjVq1NhctWrVDbYzvbJatWpj58yZ89i8efN+02sSP3/88YdZvny5uemmmzK2ff/9997noUOHms8++8yUKVMm389LI6VixYp6AAmu4KMVfbt27cbsueeerX7//fe3lAYFngZbrN1n7Tlrq0PbS1rrZe1tJ1IgQLxuram1p6z95gSLT6y1tDY1JHxca+1Za+Os7cltWZuTqkLF119/bVq0aGHWr1/veSrcdtttXkfn119/9YSL9PT0lLlXGpZHHXWUmTFjhgo1lY9FFttmM7/99pu56667MrbZ9py58sorzfPPP28++OADU7ZsWSWUUB7NR8aPH2/atGljmjRpYp5++mmvvb1o0SIzZcoUT6AXhU5Na3gEtbBWw9pu1vawttDaEms8pImubbdIQkUSYhucOzzyyCN9RowYccW1115b+owzzjDdunXzlPgaNWqYPfbYwyxcuLCUzXAV58+fX9FmyuumTZt27X777fe7/e2Vc+fOnaB8EB94TYQbEfDGG2+YDh06mFdffdVceOGF+X7Ojh07eiYSX8GHK/rRo0fv0LZt2zerVavWctWqVVOVBgWaBggKt/H6W3sytP1kV0k97z53sNbW2gm06d22QdamWBto7cAgG7nj9CgO7zbeV61bt/bKq08//dTYsn6b72+//faUut8PP/zQm5oiVD4WddLS0rZrYzAQcsEFF5gXX3zRXHbZZUokoTyajzz++OOmTp06njBRosR/UQOYeiEKlSbW7rHWKJPv93BmnJDBYNVH1ihA35NQkSS89NJLFzZt2vSJhQsXlrvvvvvMWWed5bkKbfc099jDs2OOOcacffbZZsuWLWkjR46sdeutt47dZ599fpw3b147u9uvyhc558wzz/QKt5kzZ26znTnXjz76qNdRgOOOO8706NHD2ErCGwFklOSUU07xRI4oTDNh9NM+H7xkzHfffWf69u27zT54cDzxxBOe8ss0lPbt23uNGRo6zCPnGLhpH3300Rm/4VoGDx5sOnfu7E1ZCeDaH3nkEa8DU7t2bVXwmVT0Y8aMKd2mTZvxFSpUaLFu3bp3lQYFlgYLrDHq0zUiVHQ2/tSOD9xnll/7PCRSAK4CI6w9bW0ftz+eFi3dtpQv52h4LV682LzzzjvbiRSxYFoI5cyPP/7oeY7tvPPOnqs50zCyY8WKFWbAgAFe2bJ161az6667mvPPP98gmCMePPzww+bOO+80++677za/e+qppzwPtWinLMrkyZPNuHHjzNy5c83GjRvNwQcfbG644YaM+xoxYoS5++67vXPjOQKci3MG2Aa618lDwOHe6OxR9gqVj0WBTp06mauuuspMmzYtQ6i4/vrrvQGMmjVrmnvvvdfMnj3by3M33nij9z2eVORLygDb3jOHHHKI1/6oVatWxnEnTJhgpk+f7g1s0b5A8IP69et7U8Noq4Qhjw8fPtzYd8ObqsVg2DXXXOO5yAe89dZbnvs8+W/IkCHeHPvNmzebiRMnZkyPfffdd80zzzzj5X9G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GqQaeX2vhyXqQidPnuzZY489pgIx7+VZUtWLyVo22MZrUq36URzbHKeccop54YUX8hxQN7ewtOMxxxyz3RLqRTmPplIbJgd5NNftl0TXdckAq+6wAtWFF15YVOu6vJabrPpxDBu//vrrAg2oyfFD/J4saZxSQoXNxCMnTpxYN5HLkz777LO8hG8X0v16BZt74dPiWZZRTejcM2vWLPPJJ59ss450YZNdxR7ruf/+++9vVatWrWWbNm3Gjx07tkxhVPTRDnqeWj1xiBP5kQY9evTIWKIOgojsqNDr1q3ztm3cuNHccMMNZsiQIQlNg1QjN+91okCoDJ63SMp2QJ7qRZUNanNEmT9/vhe0OOotk0iGDRtmDjroIJX1KdaGyU37pTjx2muvma5duxbnOon+pbc86ZgxYwp0eVKOHzmvhIr8Zvr06YMWLFhwu30RSqJcFTT2PH///PPPh9l/zy+M+02WAo6GFxnx/PPPN3369DFvv/22t9woBUy4sHnllVe8RhvfMfp04oknRtPTPPfcc2bUqFFeo46lZm+55RYzZ84c8/3335ubb77Z22/GjBnm0Ucf9SIxR5cKe+KJJwiq6s0J3ibH2WsaPHiwWbFiBXO/zEUXXWTatm27zT4fffSRtw/rNMP+++9vbJp6+02ZMiXDjfC6667zlvHD7euRRx4plHc9r8991apVUytUqNCidevWE958882yiazo86uCT2QafP755947vHZtRgBmU65cOa/xeuSRR2Z0RjZs2OC5eF9yySWmfv36EikS/EwLEp4x5RvvAUGnKEPg3HPPJVZRxj4DBgww7777rlee4TJOJza8pNr48ePNzJkzvbKNVSKGDx/uNdYpY+w76W3v16+ftx9lJmVh7dq1ze233+51fpliwLzYv//+24vSTeeXkdYAOlMcd9KkSd5IftmyZb1RmE6dOqVkR6cg6kWVDcnb5pg9e7YZOHCg1w6AffbZx/NWqVevXsY18jywNWvWeEvD4oVwwQUXeK7W4XxIIDzq8/79+3vPi3xIXiPP1qnje2yTl4O6n+32vfDs8ccf9zxp+D2jvohSDzzwgNfGIE8S9BHxasSIEea9994zv/76q3eM4447znTv3n27ZRuXLl3qlR28T9xDMIq87777ml69epnffvvNa59wPXD//fcX6dgZxb0NU1TECYT5559/3ivfeN+u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F8yVV14poUIIIYQQSYuECiGEEIXKTTfddFjjxo33bNGiRR37sUt6evov9u8nphiJFjmlffv2pnr16koIIYQQQqQkEiqEEEIUKn369DnYmqlYseLm5s2bL+7SpUuVFi1a1DYSLTLl3nvvVSIIIYQQImWRUCGEECIpWLt2bak33nijFpYsosXHH39sBg8ebBYsWOB9rl27trHXY9q1a+d9njhxovn222/Ntddea/r27Ws++OADb/txxx1nevTosd3SpP/++695+umnzYQJE8ymTZu86Rfsd9BBB22z39atW82wYcPM8OHDzZo1a0y5cuXM0Ucfbe666y5TsmRJT6jYeeedvSkcAbNnzzYvv/yy+eGHH8wff/zhnbtr167e9QohhBBCSKgQQgghEidaFMg1TJkyJUOUaNOmjScYTJs2zYwYMSJDqJg5c6bp37+/GTt2rBczolmzZmblypVm0KBBZuTIkebLL780VapUyRAfWrVqZb766itz6aWXmt12283bp169euadd97xxA2w92Y6duxoRo0aZc466yxz1FFHmYULF5qffvrJEylg/PjxnsgRFipuvPFG89dff5kmTZqYGjVqeNdEwE725a8QQgghhIQKIYQoRtjO5TilQu7ITmgIixaVK1dOb9++/cbOnTuf0KhRo47GD8JZILz22mvmiCOO8LwasmLFihXmoosu8lbTCDj//PM9AYJtvXv39rY9++yz5t133zVff/11RhDNq666ytSvX99cd9115rPPPvO2cT57r975O3ToEPf1IqCUKVMm4/Nll11mDjjgADN06NCEChX/b+9OwGs+0/+P30cSEUVsY40wSkaNMh2qhFTQRku1ttqi9upM+0c7xNpSE1Nqi6SWoqYyat9rKzPGhSmpdiwNqkZRKrFTIY1EnN/3firnnxBbYvme5P26ruc62zffnDwni/NxP/ejIcvcuXP5eQAAAAQVAPAoWG8ui1lvNgOYiYfjwoULjunTp/voyJcvn7NKlSoFHtTn8vf3N6HBxo0bTZXC7fTs2TPD7Ro1akhISIipakgLKubMmSPPPfdchp0+tEKiTZs2MnjwYLNco2jRoiagCAgIuKeQQqUPKVSePHmkevXqZivTh0W/hn379klu/pnQ3wn8pAIAkD0EFQBw71KsN4XX9MrOnTuL6WBKHr6kpCTHrl27HntQ5+/fv7+p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\n", + "image/png": "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94tyeLxlebiW5dl8v0CvlRBCCCFE8UIxKoQQQgghhBBCCJE0SKgQQgghhBBCCCFE0qCpH0IIkViIYzEvzn0JLPm7kkwIIYQQQhQnJFQIIURieSMH+45QcgkhhBBCiOKGpn4IIYQQQgghhBAiaZBQIYQQQgghhBBCiKRBQoUQIreUUxIoDYQQKhuEUB4VQuQ3EiqEEDnm2GOPbVu5cuXVrVu37qQ0KL5pIIRQ2SCE8qgQoiCQUCGEyHEF/8svvwy//fbbS3/yyScvnnLKKe2VBsUvDYQQKhuEUB4VQhQUEiqEEDmu4EeNGlWqe/fuZuTIkSW//fbbYQ0aNDhTaVB80kAIobJBCOVRIURBIqFCpAR///23+f3335UQCargTzrpJG8bf6nof/rpp9cPO+ywVkqD1E8DkTn2HTAXXXSRmT9/foEcf9WqVd7xv/zySyW2ygYhhPJosea6664zr732mhIihSmlJBD5yT333GNeeOGFjM/lypUz++yzjzn55JPN5ZdfbnbccccCOe8VV1xhxowZY/766y89hARV8AF8ZvuZZ545qk6dOm1mz549UWmQkDS439of1h6J8d3/rB1prZu1dFfWX2KtjbWdrM2z1s/atNBv0qx1sXaWtV2sbbD2rTVaASnbM165cqXp06ePGTt2rPn555+9bVWqVDH16tUzjLideuqpcR9r6dKlXvl39dVXm7333jvfr3XdunXe8Zs3b877qIJJZUOxo3///uaxxx7L+FypUiUvr51++umma9eupnTp0kokoTxaAEyaNMncf//9nlD+77//mp122skceeSRXtv+3HPPLZRrev31103JkiUL7fxCQkWB8PLLL5/xxRdfXDl9+vR6f/75ZwXbUC29ePHiEjVq1NhctWrVDbYzvbJatWpj58yZ89i8efN+02sSP3/88YdZvny5uemmmzK2ff/9997noUOHms8++8yUKVMm389LI6VixYp6AAmu4KMVfbt27cbsueeerX7//fe3lAYFngZbrN1n7Tlrq0PbS1rrZe1tJ1IgQLxuram1p6z95gSLT6y1tDY1JHxca+1Za+Os7cltWZuTqkLF119/bVq0aGHWr1/veSrcdtttXkfn119/9YSL9PT0lLlXGpZHHXWUmTFjhgo1lY9FFttmM7/99pu56667MrbZ9py58sorzfPPP28++OADU7ZsWSWUUB7NR8aPH2/atGljmjRpYp5++mmvvb1o0SIzZcoUT6AXhU5Na3gEtbBWw9pu1vawttDaEms8pImubbdIQkUSYhucOzzyyCN9RowYccW1115b+owzzjDdunXzlPgaNWqYPfbYwyxcuLCUzXAV58+fX9FmyuumTZt27X777fe7/e2Vc+fOnaB8EB94TYQbEfDGG2+YDh06mFdffdVceOGF+X7Ojh07eiYSX8GHK/rRo0fv0LZt2zerVavWctWqVVOVBgWaBggKt/H6W3sytP1kV0k97z53sNbW2gm06d22QdamWBto7cAgG7nj9CgO7zbeV61bt/bKq08//dTYsn6b72+//faUut8PP/zQm5oiVD4WddLS0rZrYzAQcsEFF5gXX3zRXHbZZUokoTyajzz++OOmTp06njBRosR/UQOYeiEKlSbW7rHWKJPv93BmnJDBYNVH1ihA35NQkSS89NJLFzZt2vSJhQsXlrvvvvvMWWed5bkKbfc099jDs2OOOcacffbZZsuWLWkjR46sdeutt47dZ599fpw3b147u9uvyhc558wzz/QKt5kzZ26znTnXjz76qNdRgOOOO8706NHD2ErCGwFklOSUU07xRI4oTDNh9NM+H7xkzHfffWf69u27zT54cDzxxBOe8ss0lPbt23uNGRo6zCPnGLhpH3300Rm/4VoGDx5sOnfu7E1ZCeDaH3nkEa8DU7t2bVXwmVT0Y8aMKd2mTZvxFSpUaLFu3bp3lQYFlgYLrDHq0zUiVHQ2/tSOD9xnll/7PCRSAK4CI6w9bW0ftz+eFi3dtpQv52h4LV682LzzzjvbiRSxYFoI5cyPP/7oeY7tvPPOnqs50zCyY8WKFWbAgAFe2bJ161az6667mvPPP98gmCMePPzww+bOO+80++677za/e+qppzwPtWinLMrkyZPNuHHjzNy5c83GjRvNwQcfbG644YaM+xoxYoS5++67vXPjOQKci3MG2Aa618lDwOHe6OxR9gqVj0WBTp06mauuuspMmzYtQ6i4/vrrvQGMmjVrmnvvvdfMnj3by3M33nij9z2eVORLygDb3jOHHHKI1/6oVatWxnEnTJhgpk+f7g1s0b5A8IP69et7U8Noq4Qhjw8fPtzYd8ObqsVg2DXXXOO5yAe89dZbnvs8+W/IkCHeHPvNmzebiRMnZkyPfffdd80zzzzj5X9G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GqQaeX2vhyXqQidPnuzZY489pgIx7+VZUtWLyVo22MZrUq36URzbHKeccop54YUX8hxQN7ewtOMxxxyz3RLqRTmPplIbJgd5NNftl0TXdckAq+6wAtWFF15YVOu6vJabrPpxDBu//vrrAg2oyfFD/J4saZxSQoXNxCMnTpxYN5HLkz777LO8hG8X0v16BZt74dPiWZZRTejcM2vWLPPJJ59ss450YZNdxR7ruf/+++9vVatWrWWbNm3Gjx07tkxhVPTRDnqeWj1xiBP5kQY9evTIWKIOgojsqNDr1q3ztm3cuNHccMMNZsiQIQlNg1QjN+91okCoDJ63SMp2QJ7qRZUNanNEmT9/vhe0OOotk0iGDRtmDjroIJX1KdaGyU37pTjx2muvma5duxbnOon+pbc86ZgxYwp0eVKOHzmvhIr8Zvr06YMWLFhwu30RSqJcFTT2PH///PPPh9l/zy+M+02WAo6GFxnx/PPPN3369DFvv/22t9woBUy4sHnllVe8RhvfMfp04oknRtPTPPfcc2bUqFFeo46lZm+55RYzZ84c8/3335ubb77Z22/GjBnm0Ucf9SIxR5cKe+KJJwiq6s0J3ibH2WsaPHiwWbFiBXO/zEUXXWTatm27zT4fffSRtw/rNMP+++9vbJp6+02ZMiXDjfC6667zlvHD7euRRx4plHc9r8991apVUytUqNCidevWE958882yiazo86uCT2QafP755947vHZtRgBmU65cOa/xeuSRR2Z0RjZs2OC5eF9yySWmfv36EikS/EwLEp4x5RvvAUGnKEPg3HPPJVZRxj4DBgww7777rlee4TJOJza8pNr48ePNzJkzvbKNVSKGDx/uNdYpY+w76W3v16+ftx9lJmVh7dq1ze233+51fpliwLzYv//+24vSTeeXkdYAOlMcd9KkSd5IftmyZb1RmE6dOqVkR6cg6kWVDcnb5pg9e7YZOHCg1w6AffbZx/NWqVevXsY18jywNWvWeEvD4oVwwQUXeK7W4XxIIDzq8/79+3vPi3xIXiPP1qnje2yTl4O6n+32vfDs8ccf9zxp+D2jvohSDzzwgNfGIE8S9BHxasSIEea9994zv/76q3eM4447znTv3n27ZRuXLl3qlR28T9xDMIq87777ml69epnffvvNa59wPXD//fcX6dgZxb0NU1TECYT5559/3ivfeN+u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F8yVV14poUIIIYQQSYuECiGEEIXKTTfddFjjxo33bNGiRR37sUt6evov9u8nphiJFjmlffv2pnr16koIIYQQQqQkEiqEEEIUKn369DnYmqlYseLm5s2bL+7SpUuVFi1a1DYSLTLl3nvvVSIIIYQQImWRUCGEECIpWLt2bak33nijFpYsosXHH39sBg8ebBYsWOB9rl27trHXY9q1a+d9njhxovn222/Ntddea/r27Ws++OADb/txxx1nevTosd3SpP/++695+umnzYQJE8ymTZu86Rfsd9BBB22z39atW82wYcPM8OHDzZo1a0y5cuXM0Ucfbe666y5TsmRJT6jYeeedvSkcAbNnzzYvv/yy+eGHH8wff/zhnbtr167e9QohhBBCSKgQQgghEidaFMg1TJkyJUOUaNOmjScYTJs2zYwYMSJDqJg5c6bp37+/GTt2rBczolmzZmblypVm0KBBZuTIkebLL780VapUyRAfWrVqZb766itz6aWXmt12283bp169euadd97xxA2w92Y6duxoRo0aZc466yxz1FFHmYULF5qffvrJEylg/PjxnsgRFipuvPFG89dff5kmTZqYGjVqeNdEwE725a8QQgghhIQKIYQoRtjO5TilQu7ITmgIixaVK1dOb9++/cbOnTuf0KhRo47GD8JZILz22mvmiCOO8LwasmLFihXmoosu8lbTCDj//PM9AYJtvXv39rY9++yz5t133zVff/11RhDNq666ytSvX99cd9115rPPPvO2cT57r975O3ToEPf1IqCUKVMm4/Nll11mDjjgADN06NCEChX/b+9OwGs+0/+P30cSEUVsY40wSkaNMh2qhFTQRku1ttqi9upM+0c7xNpSE1Nqi6SWoqYyat9rKzPGhSmpdiwNqkZRKrFTIY1EnN/3firnnxBbYvme5P26ruc62zffnDwni/NxP/ejIcvcuXP5eQAAAAQVAPAoWG8ui1lvNgOYiYfjwoULjunTp/voyJcvn7NKlSoFHtTn8vf3N6HBxo0bTZXC7fTs2TPD7Ro1akhISIipakgLKubMmSPPPfdchp0+tEKiTZs2MnjwYLNco2jRoiagCAgIuKeQQqUPKVSePHmkevXqZivTh0W/hn379klu/pnQ3wn8pAIAkD0EFQBw71KsN4XX9MrOnTuL6WBKHr6kpCTHrl27HntQ5+/fv7+p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", "text/plain": [ "" ] @@ -488,7 +488,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.2" + "version": "3.12.1" } }, "nbformat": 4, From 7ae2895b1b5779bfb7d9923e8e3a29ab7b3dd3a5 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 28 Feb 2024 15:09:44 +0100 Subject: [PATCH 03/54] Tried a new heurstic --- explainer/explainer.py | 94 ++++++++++++++------- explainer/tutorial/explainer_tutorial.ipynb | 70 +++++++++++++-- 2 files changed, 125 insertions(+), 39 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 76a6350..7b2e589 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -1,6 +1,6 @@ import itertools import re -from itertools import combinations +from itertools import combinations, permutations, product class Trace: def __init__(self, nodes): @@ -39,6 +39,7 @@ def __init__(self): self.constraint_fulfillment_alpha = 1 self.repetition_alpha = 1 self.sub_trace_adherence_alpha = 1 + self.adherent_trace = None def set_heuristic_alpha(self, constraint_fulfillment_alpha = 1, repetition_alpha = 1, sub_trace_adherence_alpha = 1): self.constraint_fulfillment_alpha = constraint_fulfillment_alpha @@ -53,6 +54,9 @@ def add_constraint(self, regex): :param regex: A regular expression representing the constraint. """ self.constraints.append(regex) + if self.contradiction(): + self.constraints.remove(regex) + print(f"Constraint {regex} contradicts the other constraints.") # Extract unique characters (nodes) from the regex and update the nodes set unique_nodes = set(filter(str.isalpha, regex)) self.nodes.update(unique_nodes) @@ -96,6 +100,18 @@ def conformant(self, trace): trace_str = ''.join(trace) return all(re.search(constraint, trace_str) for constraint in self.constraints) + def contradiction(self): + nodes = self.get_nodes_from_constraint() + max_length = 10 # Set a reasonable max length to avoid infinite loops + + for length in range(1, max_length + 1): + for combination in product(nodes, repeat=length): + test_str = ''.join(combination) + if all(re.search(con, test_str) for con in self.constraints): + self.adherent_trace = test_str + return False # Found a match + return True # No combination satisfied all constraints + def minimal_expl(self, trace): """ @@ -144,25 +160,25 @@ def counterfactual_expl(self, trace): } lowest_heuristic, lowest_score = min(scores.items(), key=lambda x: x[1]) - + lowest_heuristic = 'similarity' + lowest_score = self.evaluate_similarity(trace) # Perform operation based on the lowest scoring heuristic if lowest_heuristic: - return self.operate_on_trace(trace, lowest_heuristic, lowest_score, "") + return self.operate_on_trace(trace, lowest_heuristic, lowest_score, f'{trace.nodes}') else: return "Error identifying the lowest scoring heuristic." def counter_factual_helper(self, working_trace, explanation, depth = 0): if self.conformant(working_trace): - print(depth) - return explanation + return f'{explanation}\n{working_trace.nodes}' if depth > 100: return f'{explanation}\n Maximum depth of {depth -1} reached' # Evaluate heuristic for original trace constraint_fulfillment_score = 1 if len(self.constraints) > 1: - constraint_fulfillment_score = self.evaluate(working_trace, "constraint_fulfillment") - sub_trace_adherence_score = self.evaluate(working_trace, "sub_trace_adherence") - repetition_score = self.evaluate(working_trace, "repetition") + constraint_fulfillment_score = 1#self.evaluate(working_trace, "constraint_fulfillment") + sub_trace_adherence_score = 1# self.evaluate(working_trace, "sub_trace_adherence") + repetition_score = 1 #self.evaluate(working_trace, "repetition") if constraint_fulfillment_score == 0 and sub_trace_adherence_score == 0: self.constraint_fulfillment_alpha = 1 self.sub_trace_adherence_alpha = 1 @@ -175,6 +191,8 @@ def counter_factual_helper(self, working_trace, explanation, depth = 0): } lowest_heuristic, lowest_score = min(scores.items(), key=lambda x: x[1]) + lowest_heuristic = 'similarity' + lowest_score = self.evaluate_similarity(working_trace) # Perform operation based on the lowest scoring heuristic if lowest_heuristic: return self.operate_on_trace(working_trace, lowest_heuristic, lowest_score, explanation, depth) @@ -214,7 +232,20 @@ def get_nodes_from_constraint(self, constraint = None): return list(set(all_nodes)) else: return list(set(re.findall(r'[A-Za-z]', constraint))) - + def get_nodes_from_constraint_ordered(self, constraint = None): + """ + Extracts unique nodes from a constraint pattern in order. + + :param constraint: The constraint pattern as a string. + :return: A list of unique nodes found within the constraint. + """ + if constraint is None: + all_nodes = set() + for constr in self.constraints: + all_nodes.update(re.findall(r'[A-Za-z]+', constr)) + return list(all_nodes) + else: + return list(re.findall(r'[A-Za-z]', constraint)) def modify_subtrace(self, trace): add_mod = self.addition_modification(trace) @@ -231,10 +262,8 @@ def addition_modification(self, trace): counterfactuals = [] possible_additions = self.get_nodes_from_constraint() - # Only add one node at a time for added_node in possible_additions: for insertion_point in range(len(trace.nodes) + 1): - # Create a new trace with the added node new_trace_nodes = trace.nodes[:insertion_point] + [added_node] + trace.nodes[insertion_point:] new_trace_str = f"Addition (Added {added_node} at position {insertion_point}): " + "->".join(new_trace_nodes) @@ -262,6 +291,8 @@ def subtraction_modification(self, trace): def evaluate(self, trace, heurstic): if heurstic == "constraint_fulfillment": return self.evaluate_constraint_fulfillment(trace) + elif heurstic == "similarity": + return self.evaluate_similarity(trace) elif heurstic == "sub_trace_adherence": return self.evaluate_sub_trace_adherence(trace) elif heurstic == "repetition": @@ -269,6 +300,14 @@ def evaluate(self, trace, heurstic): else: return "No valid evaluation method" + def evaluate_similarity(self, trace): + length = len(self.adherent_trace) + trace_len = len("".join(trace)) + lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) + max_distance = max(length, trace_len) + normalized_score = 1 - lev_distance / max_distance + return normalized_score + def evaluate_constraint_fulfillment(self, optional_trace): if self.constraint_fulfillment_alpha == 0: return 0 @@ -287,18 +326,13 @@ def evaluate_repetition(self, trace): else: node_counts[node] = 1 - # Calculate the deviation of each node's occurrence from 1 deviations = [count - 1 for count in node_counts.values()] - # Normalize deviation: Here, we take the sum of deviations and divide by the total number of nodes - # This gives an average deviation per node, which we normalize by dividing by the length of the trace - # This assumes the worst case where every node in the trace is different and repeated once if trace.nodes: normalized_deviation = sum(deviations) / len(trace.nodes) else: normalized_deviation = 0 - # Ensure the score is between 0 and 1 normalized_deviation = 1 - min(max(normalized_deviation, 0), 1) return normalized_deviation * self.repetition_alpha @@ -336,6 +370,7 @@ def get_sublists1(lst, n): Generates all possible non-empty contiguous sublists of a list, maintaining order. :param lst: The input list. + n: the minmum length of sublists :return: A list of all non-empty contiguous sublists. """ sublists = [] @@ -345,7 +380,6 @@ def get_sublists1(lst, n): sub = lst[i:j] sublists.append(sub) return sublists - def levenshtein_distance(seq1, seq2): """ Calculates the Levenshtein distance between two sequences. @@ -364,20 +398,18 @@ def levenshtein_distance(seq1, seq2): matrix[x][y] = matrix[x-1][y-1] else: matrix[x][y] = min( - matrix[x-1][y] + 1, # Deletion - matrix[x][y-1] + 1, # Insertion - matrix[x-1][y-1] + 1 # Substitution + matrix[x-1][y] + 1, + matrix[x][y-1] + 1, + matrix[x-1][y-1] + 1 ) return matrix[size_x-1][size_y-1] - +""" exp = Explainer() -exp.add_constraint('A.*B.*C.*D') -#exp.add_constraint('A.*B.*C') -#exp.add_constraint('A.*B') -#optional_trace = Trace(['A', 'B', 'C', 'E', 'E']) -optional_trace = Trace(['A', 'B', 'B']) -print(exp.evaluate_repetition(optional_trace)) - - -#print(exp.counterfactual_expl(optional_trace)) - +exp.add_constraint('B.*A.*B.*C') +exp.add_constraint('A.*B.*C.*') +exp.add_constraint('A.*D.*B*') +exp.add_constraint('A[^D]*B') +optional_trace = Trace(['A']) + +print(exp.counterfactual_expl(optional_trace)) +""" \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index bc67d3a..a57f21a 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -95,23 +95,56 @@ "text": [ "Constraint: A.*B.*C\n", "Trace:['A', 'C']\n", - "Suggested change to make the trace (['A', 'C']) conformant: Addition: A->B->C\n", + "['A', 'C']\n", + "Addition (Added B at position 1): A->B->C, based on heurstic similarity\n", + "['A', 'B', 'C']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['C', 'B', 'A']\n", - "Suggested change to make the trace (['C', 'B', 'A']) conformant: Reordering: A->B->C\n", + "['C', 'B', 'A']\n", + "Addition (Added A at position 0): A->C->B->A, based on heurstic similarity\n", + "Subtraction (Removed C): A->B->A, based on heurstic similarity\n", + "Addition (Added C at position 2): A->B->C->A, based on heurstic similarity\n", + "['A', 'B', 'C', 'A']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'A', 'C']\n", - "Suggested change to make the trace (['A', 'A', 'C']) conformant: Substitution: Replace A with B at position 2\n", + "['A', 'A', 'C']\n", + "Addition (Added B at position 1): A->B->A->C, based on heurstic similarity\n", + "['A', 'B', 'A', 'C']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", + "-----------\n", + "Constraint: A.*B.*C\n", + "Trace:['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", + "['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", + "Subtraction (Removed A->A->C->TEST->X->Y): A->A->C, based on heurstic similarity\n", + "Addition (Added B at position 1): A->B->A->C, based on heurstic similarity\n", + "['A', 'B', 'A', 'C']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", "-----------\n", "Constraint: AC\n", "Trace:['A', 'X', 'C']\n", - "Suggested change to make the trace (['A', 'X', 'C']) conformant: Subtraction (Removed X): A->C\n", - "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n" + "['A', 'X', 'C']\n", + "Subtraction (Removed X): A->C, based on heurstic similarity\n", + "['A', 'C']\n", + "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", + "-----------\n", + "constraint: AC\n", + "constraint: B.*A.*B.*C\n", + "constraint: A.*B.*C.*\n", + "constraint: A.*D.*B*\n", + "constraint: A[^D]*B\n", + "Trace:['A', 'X', 'C']\n", + "['A', 'X', 'C']\n", + "Addition (Added AC at position 0): AC->A->X->C, based on heurstic similarity\n", + "Subtraction (Removed X): AC->A->C, based on heurstic similarity\n", + "Addition (Added B at position 0): B->AC->A->C, based on heurstic similarity\n", + "Addition (Added B at position 2): B->AC->B->A->C, based on heurstic similarity\n", + "Addition (Added D at position 5): B->AC->B->A->C->D, based on heurstic similarity\n", + "['B', 'AC', 'B', 'A', 'C', 'D']\n", + "Non-conformance due to: Constraint (A[^D]*B) is violated by subtrace: ('A', 'X')\n" ] } ], @@ -119,23 +152,32 @@ "non_conformant_trace = Trace(['A', 'C'])\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.counterfactual_expl(non_conformant_trace)) # Addition\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "\n", + "non_conformant_trace = Trace(['C', 'B', 'A'])\n", + "print('-----------')\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", "\n", - "non_conformant_trace = Trace(['C', 'B', 'A']) #Reordering\n", + "non_conformant_trace = Trace(['A','A','C'])\n", "print('-----------')\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", "\n", - "non_conformant_trace = Trace(['A','A','C']) #Substitution\n", + "\n", + "non_conformant_trace = Trace(['A','A','C','A','TEST','A','C', 'X', 'Y']) \n", "print('-----------')\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", "\n", + "\n", "explainer.remove_constraint(0)\n", "explainer.add_constraint('AC')\n", "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", @@ -145,6 +187,18 @@ "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", "\n", + "explainer.add_constraint('B.*A.*B.*C')\n", + "explainer.add_constraint('A.*B.*C.*')\n", + "explainer.add_constraint('A.*D.*B*')\n", + "explainer.add_constraint('A[^D]*B')\n", + "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", + "print('-----------')\n", + "for con in explainer.constraints:\n", + " print(f'constraint: {con}')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "\n", "\n" ] }, From c0b4ccf0c211c7254bb56d544970ef39825aa5b7 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 1 Mar 2024 10:52:39 +0100 Subject: [PATCH 04/54] Some code clean up --- .gitignore | 3 +- explainer/explainer.py | 231 ++++---------------- explainer/tutorial/explainer_tutorial.ipynb | 33 ++- 3 files changed, 58 insertions(+), 209 deletions(-) diff --git a/.gitignore b/.gitignore index 8a44e3c..d34c252 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,5 @@ dmypy.json .vscode/ # explainer Stuff -explainer/test.py \ No newline at end of file +explainer/test.py +explainer/old_code.py \ No newline at end of file diff --git a/explainer/explainer.py b/explainer/explainer.py index 7b2e589..5a4feb5 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -35,17 +35,7 @@ def __init__(self): Initializes an Explainer instance. """ self.constraints = [] # List to store constraints (regex patterns) - self.nodes = set() # Set to store unique nodes involved in constraints - self.constraint_fulfillment_alpha = 1 - self.repetition_alpha = 1 - self.sub_trace_adherence_alpha = 1 self.adherent_trace = None - - def set_heuristic_alpha(self, constraint_fulfillment_alpha = 1, repetition_alpha = 1, sub_trace_adherence_alpha = 1): - self.constraint_fulfillment_alpha = constraint_fulfillment_alpha - self.repetition_alpha = repetition_alpha - self.sub_trace_adherence_alpha - return def add_constraint(self, regex): """ @@ -57,9 +47,6 @@ def add_constraint(self, regex): if self.contradiction(): self.constraints.remove(regex) print(f"Constraint {regex} contradicts the other constraints.") - # Extract unique characters (nodes) from the regex and update the nodes set - unique_nodes = set(filter(str.isalpha, regex)) - self.nodes.update(unique_nodes) def remove_constraint(self, idx): """ @@ -138,74 +125,29 @@ def minimal_expl(self, trace): return "Trace is non-conformant, but the specific constraint violation could not be determined." def counterfactual_expl(self, trace): - """ - 3 Heuristics: - Constraint fulfillment - Prioritize modifications that maximize the number of constraints the trace fulfills. - Minimal Deviation - Seek the least number of changes necessary to make the trace adhere to constraints. - Sub-trace Adherence - Evaluate the proportion of the trace that adheres to constraints after each modification. - """ if self.conformant(trace): return "The trace is already conformant, no changes needed." - # Evaluate heuristic for original trace - constraint_fulfillment_score = 1 - if len(self.constraints) > 1: - constraint_fulfillment_score = self.evaluate(trace, "constraint_fulfillment") - sub_trace_adherence_score = self.evaluate(trace, "sub_trace_adherence") - repetition_score = self.evaluate(trace, "repetition") - # Identify the lowest score and the corresponding heuristic - scores = { - 'constraint_fulfillment': constraint_fulfillment_score, - 'sub_trace_adherence': sub_trace_adherence_score, - 'repetition' : repetition_score - } - - lowest_heuristic, lowest_score = min(scores.items(), key=lambda x: x[1]) - lowest_heuristic = 'similarity' - lowest_score = self.evaluate_similarity(trace) + score = self.evaluate_similarity(trace) # Perform operation based on the lowest scoring heuristic - if lowest_heuristic: - return self.operate_on_trace(trace, lowest_heuristic, lowest_score, f'{trace.nodes}') - else: - return "Error identifying the lowest scoring heuristic." + return self.operate_on_trace(trace, score, f'{trace.nodes}') + def counter_factual_helper(self, working_trace, explanation, depth = 0): if self.conformant(working_trace): return f'{explanation}\n{working_trace.nodes}' if depth > 100: return f'{explanation}\n Maximum depth of {depth -1} reached' - # Evaluate heuristic for original trace - constraint_fulfillment_score = 1 - if len(self.constraints) > 1: - constraint_fulfillment_score = 1#self.evaluate(working_trace, "constraint_fulfillment") - sub_trace_adherence_score = 1# self.evaluate(working_trace, "sub_trace_adherence") - repetition_score = 1 #self.evaluate(working_trace, "repetition") - if constraint_fulfillment_score == 0 and sub_trace_adherence_score == 0: - self.constraint_fulfillment_alpha = 1 - self.sub_trace_adherence_alpha = 1 - return self.counter_factual_helper(working_trace, explanation, depth + 1) - # Identify the lowest score and the corresponding heuristic - scores = { - 'sub_trace_adherence': sub_trace_adherence_score, - 'repetition' : repetition_score, - 'constraint_fulfillment': constraint_fulfillment_score, - } - - lowest_heuristic, lowest_score = min(scores.items(), key=lambda x: x[1]) - lowest_heuristic = 'similarity' - lowest_score = self.evaluate_similarity(working_trace) - # Perform operation based on the lowest scoring heuristic - if lowest_heuristic: - return self.operate_on_trace(working_trace, lowest_heuristic, lowest_score, explanation, depth) - else: - return "Error identifying the lowest scoring heuristic." + score = self.evaluate_similarity(working_trace) + return self.operate_on_trace(working_trace, score, explanation, depth) - def operate_on_trace(self, trace, heuristic, score, explanation_path, depth = 0): + + def operate_on_trace(self, trace, score, explanation_path, depth = 0): explanation = None counter_factuals = self.modify_subtrace(trace) best_subtrace = None best_score = -float('inf') for subtrace in counter_factuals: - current_score = self.evaluate(subtrace[0], heuristic) + current_score = self.evaluate_similarity(subtrace[0]) if current_score > best_score and current_score > score: best_score = current_score best_subtrace = subtrace[0] @@ -213,9 +155,8 @@ def operate_on_trace(self, trace, heuristic, score, explanation_path, depth = 0) if best_subtrace == None: for subtrace in counter_factuals: print(subtrace[0].nodes) - print(heuristic) - self.operate_on_trace(subtrace[0], heuristic, score, explanation_path, depth + 1) - explanation_string = explanation_path + '\n' + str(explanation) + f", based on heurstic {heuristic}" + self.operate_on_trace(subtrace[0], score, explanation_path, depth + 1) + explanation_string = explanation_path + '\n' + str(explanation) return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) def get_nodes_from_constraint(self, constraint = None): @@ -228,78 +169,43 @@ def get_nodes_from_constraint(self, constraint = None): if constraint is None: all_nodes = set() for constr in self.constraints: - all_nodes.update(re.findall(r'[A-Za-z]+', constr)) + all_nodes.update(re.findall(r'[A-Za-z]', constr)) return list(set(all_nodes)) else: return list(set(re.findall(r'[A-Za-z]', constraint))) - def get_nodes_from_constraint_ordered(self, constraint = None): - """ - Extracts unique nodes from a constraint pattern in order. - :param constraint: The constraint pattern as a string. - :return: A list of unique nodes found within the constraint. - """ - if constraint is None: - all_nodes = set() - for constr in self.constraints: - all_nodes.update(re.findall(r'[A-Za-z]+', constr)) - return list(all_nodes) - else: - return list(re.findall(r'[A-Za-z]', constraint)) def modify_subtrace(self, trace): - - add_mod = self.addition_modification(trace) - sub_mod = self.subtraction_modification(trace) - - return sub_mod + add_mod - - from itertools import combinations, chain - - def addition_modification(self, trace): - """ - Suggests additions to the trace to meet constraints, but only one node at a time. """ - counterfactuals = [] - possible_additions = self.get_nodes_from_constraint() + Modifies the given trace to meet constraints by adding nodes where the pattern fails. - for added_node in possible_additions: - for insertion_point in range(len(trace.nodes) + 1): - new_trace_nodes = trace.nodes[:insertion_point] + [added_node] + trace.nodes[insertion_point:] - new_trace_str = f"Addition (Added {added_node} at position {insertion_point}): " + "->".join(new_trace_nodes) - - counterfactuals.append((Trace(new_trace_nodes), new_trace_str)) - - return counterfactuals + Parameters: + - trace: A list of node identifiers - - def subtraction_modification(self, trace): + Returns: + - A list of potential subtraces each modified to meet constraints. """ - Suggests node removals from the trace for conformance. - """ - counterfactuals = [] - - for r in range(1, len(trace.nodes)): - for indices_to_remove in combinations(range(len(trace.nodes)), r): - modified_trace_nodes = [node for index, node in enumerate(trace.nodes) if index not in indices_to_remove] - - removed_nodes_str = "->".join([trace.nodes[index] for index in indices_to_remove]) - new_trace_str = f"Subtraction (Removed {removed_nodes_str}): " + "->".join(modified_trace_nodes) + potential_subtraces = [] + possible_additions = self.get_nodes_from_constraint() + for i, s_trace in enumerate(get_iterative_subtrace(trace)): + for con in self.constraints: + new_trace_str = "".join(s_trace) + match = re.match(new_trace_str, con) + if not match: + for add in possible_additions: - counterfactuals.append((Trace(modified_trace_nodes), new_trace_str)) - return counterfactuals + potential_subtraces.append([Trace(s_trace + [add] + trace.nodes[i+1:]), + f"Addition (Added {add} at position {i+1}): " + "->" + .join(s_trace + [add] + trace.nodes[i+1:])]) + potential_subtraces.append([Trace(s_trace[:-1] + [add] + trace.nodes[i:]), + f"Addition (Added {add} at position {i+1}): " + "->" + .join(s_trace + [add] + trace.nodes[i+1:])]) + + potential_subtraces.append([Trace(s_trace[:-1] + trace.nodes[i+1:]), + f"Subtraction (Removed {s_trace[i]} from position {i}): " + "->". + join(s_trace[:-1] + trace.nodes[i+1:])]) + + return potential_subtraces - def evaluate(self, trace, heurstic): - if heurstic == "constraint_fulfillment": - return self.evaluate_constraint_fulfillment(trace) - elif heurstic == "similarity": - return self.evaluate_similarity(trace) - elif heurstic == "sub_trace_adherence": - return self.evaluate_sub_trace_adherence(trace) - elif heurstic == "repetition": - return self.evaluate_repetition(trace) - else: - return "No valid evaluation method" - def evaluate_similarity(self, trace): length = len(self.adherent_trace) trace_len = len("".join(trace)) @@ -308,52 +214,6 @@ def evaluate_similarity(self, trace): normalized_score = 1 - lev_distance / max_distance return normalized_score - def evaluate_constraint_fulfillment(self, optional_trace): - if self.constraint_fulfillment_alpha == 0: - return 0 - fulfilled_constraints = sum(1 for constraint in self.constraints if re.search(constraint,"".join(optional_trace))) - total_constraints = len(self.constraints) - return (fulfilled_constraints / total_constraints) * self.constraint_fulfillment_alpha if total_constraints else 0 - - def evaluate_repetition(self, trace): - if self.repetition_alpha == 0: - return 1 - - node_counts = {} - for node in trace.nodes: - if node in node_counts: - node_counts[node] += 1 - else: - node_counts[node] = 1 - - deviations = [count - 1 for count in node_counts.values()] - - if trace.nodes: - normalized_deviation = sum(deviations) / len(trace.nodes) - else: - normalized_deviation = 0 - - normalized_deviation = 1 - min(max(normalized_deviation, 0), 1) - - return normalized_deviation * self.repetition_alpha - - - def evaluate_sub_trace_adherence(self, optional_trace): - - sub_lists = list(set([node for node in optional_trace])) - adherence_scores = [[0 for _ in self.constraints] for _ in sub_lists] - for i, sub_trace in enumerate(sub_lists): - trace_string = "".join(sub_trace) - for j, con in enumerate(self.constraints): - match = re.search(trace_string, con) - if match: - adherence_scores[i][j] = 1 - num_nodes = len(self.get_nodes_from_constraint()) - total_scores = sum(sum(row) for row in adherence_scores) - - average_score = total_scores / num_nodes if num_nodes else 0 - return average_score * self.sub_trace_adherence_alpha - def get_sublists(lst): """ Generates all possible non-empty sublists of a list. @@ -365,7 +225,7 @@ def get_sublists(lst): for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n sublists.extend(combinations(lst, r)) return sublists -def get_sublists1(lst, n): +def get_iterative_subtrace(trace): """ Generates all possible non-empty contiguous sublists of a list, maintaining order. @@ -374,11 +234,9 @@ def get_sublists1(lst, n): :return: A list of all non-empty contiguous sublists. """ sublists = [] - for i in range(len(lst)): - - for j in range(i + 2, min(i + n + 1, len(lst) + 1)): - sub = lst[i:j] - sublists.append(sub) + for i in range(0, len(trace)): + sublists.append(trace.nodes[0:i+1]) + return sublists def levenshtein_distance(seq1, seq2): """ @@ -403,13 +261,4 @@ def levenshtein_distance(seq1, seq2): matrix[x-1][y-1] + 1 ) return matrix[size_x-1][size_y-1] -""" -exp = Explainer() -exp.add_constraint('B.*A.*B.*C') -exp.add_constraint('A.*B.*C.*') -exp.add_constraint('A.*D.*B*') -exp.add_constraint('A[^D]*B') -optional_trace = Trace(['A']) -print(exp.counterfactual_expl(optional_trace)) -""" \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index a57f21a..65bbf60 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -96,38 +96,38 @@ "Constraint: A.*B.*C\n", "Trace:['A', 'C']\n", "['A', 'C']\n", - "Addition (Added B at position 1): A->B->C, based on heurstic similarity\n", + "Addition (Added B at position 2): A->C->B\n", "['A', 'B', 'C']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['C', 'B', 'A']\n", "['C', 'B', 'A']\n", - "Addition (Added A at position 0): A->C->B->A, based on heurstic similarity\n", - "Subtraction (Removed C): A->B->A, based on heurstic similarity\n", - "Addition (Added C at position 2): A->B->C->A, based on heurstic similarity\n", + "Addition (Added A at position 1): C->A->B->A\n", + "Subtraction (Removed C from position 0): A->B->A\n", + "Addition (Added C at position 2): A->B->C->A\n", "['A', 'B', 'C', 'A']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'A', 'C']\n", "['A', 'A', 'C']\n", - "Addition (Added B at position 1): A->B->A->C, based on heurstic similarity\n", - "['A', 'B', 'A', 'C']\n", + "Addition (Added B at position 2): A->A->B->C\n", + "['A', 'A', 'B', 'C']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", "['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", - "Subtraction (Removed A->A->C->TEST->X->Y): A->A->C, based on heurstic similarity\n", - "Addition (Added B at position 1): A->B->A->C, based on heurstic similarity\n", - "['A', 'B', 'A', 'C']\n", + "Subtraction (Removed TEST from position 4): A->A->C->A->A->C->X->Y\n", + "Addition (Added B at position 2): A->A->B->C->A->A->C->X->Y\n", + "['A', 'A', 'B', 'C', 'A', 'A', 'C', 'X', 'Y']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", "-----------\n", "Constraint: AC\n", "Trace:['A', 'X', 'C']\n", "['A', 'X', 'C']\n", - "Subtraction (Removed X): A->C, based on heurstic similarity\n", + "Subtraction (Removed X from position 1): A->C\n", "['A', 'C']\n", "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", "-----------\n", @@ -138,12 +138,11 @@ "constraint: A[^D]*B\n", "Trace:['A', 'X', 'C']\n", "['A', 'X', 'C']\n", - "Addition (Added AC at position 0): AC->A->X->C, based on heurstic similarity\n", - "Subtraction (Removed X): AC->A->C, based on heurstic similarity\n", - "Addition (Added B at position 0): B->AC->A->C, based on heurstic similarity\n", - "Addition (Added B at position 2): B->AC->B->A->C, based on heurstic similarity\n", - "Addition (Added D at position 5): B->AC->B->A->C->D, based on heurstic similarity\n", - "['B', 'AC', 'B', 'A', 'C', 'D']\n", + "Addition (Added C at position 1): A->C->X->C\n", + "Addition (Added B at position 1): A->B->C->X->C\n", + "Addition (Added D at position 5): B->A->C->X->C->D\n", + "Addition (Added B at position 3): B->A->C->B->X->C->D\n", + "['B', 'A', 'C', 'B', 'X', 'C', 'D']\n", "Non-conformance due to: Constraint (A[^D]*B) is violated by subtrace: ('A', 'X')\n" ] } @@ -206,7 +205,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 5: Coming soon" + "## Step 5: Signal (Not implemented yet)" ] } ], From 956fdc1517aec74f5f8c65b8a8b9ab7aa9f653ac Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 1 Mar 2024 13:03:07 +0100 Subject: [PATCH 05/54] Slight change in logic --- explainer/explainer.py | 4 ++-- explainer/tutorial/explainer_tutorial.ipynb | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 5a4feb5..a1b4804 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -197,8 +197,8 @@ def modify_subtrace(self, trace): f"Addition (Added {add} at position {i+1}): " + "->" .join(s_trace + [add] + trace.nodes[i+1:])]) potential_subtraces.append([Trace(s_trace[:-1] + [add] + trace.nodes[i:]), - f"Addition (Added {add} at position {i+1}): " + "->" - .join(s_trace + [add] + trace.nodes[i+1:])]) + f"Addition (Added {add} at position {i}): " + "->" + .join(s_trace[:-1] + [add] + trace.nodes[i:])]) potential_subtraces.append([Trace(s_trace[:-1] + trace.nodes[i+1:]), f"Subtraction (Removed {s_trace[i]} from position {i}): " + "->". diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index 65bbf60..6cb27c0 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -96,7 +96,7 @@ "Constraint: A.*B.*C\n", "Trace:['A', 'C']\n", "['A', 'C']\n", - "Addition (Added B at position 2): A->C->B\n", + "Addition (Added B at position 1): A->B->C\n", "['A', 'B', 'C']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", "-----------\n", @@ -139,10 +139,10 @@ "Trace:['A', 'X', 'C']\n", "['A', 'X', 'C']\n", "Addition (Added C at position 1): A->C->X->C\n", - "Addition (Added B at position 1): A->B->C->X->C\n", - "Addition (Added D at position 5): B->A->C->X->C->D\n", - "Addition (Added B at position 3): B->A->C->B->X->C->D\n", - "['B', 'A', 'C', 'B', 'X', 'C', 'D']\n", + "Addition (Added B at position 0): B->A->C->X->C\n", + "Addition (Added B at position 3): B->A->C->B->X->C\n", + "Addition (Added D at position 4): B->A->C->B->D->X->C\n", + "['B', 'A', 'C', 'B', 'D', 'X', 'C']\n", "Non-conformance due to: Constraint (A[^D]*B) is violated by subtrace: ('A', 'X')\n" ] } From 31fab7017aa56960936afd07463716793ca2ec2a Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 7 Mar 2024 12:19:24 +0100 Subject: [PATCH 06/54] added activation feature --- explainer/explainer.py | 45 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index a1b4804..43c8a23 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -74,8 +74,13 @@ def activation(self, trace): :param trace: A Trace instance. :return: Boolean indicating if any constraint is activated. """ - trace_str = ''.join(trace.nodes) - return any(re.search(constraint, trace_str) for constraint in self.constraints) + con_activation = [0] * len(self.constraints) + for idx, con in enumerate(self.constraints): + for event in trace.nodes: + if event in con: + con_activation[idx] = 1 + continue + return con_activation def conformant(self, trace): """ @@ -84,6 +89,13 @@ def conformant(self, trace): :param trace: A Trace instance. :return: Boolean indicating if the trace is conformant with all constraints. """ + activation = self.activation(trace) + if any(value == 0 for value in activation): + new_explainer = Explainer() + for idx, value in enumerate(activation): + if value == 1: + new_explainer.add_constraint(self.constraints[idx]) + return new_explainer.conformant(trace) trace_str = ''.join(trace) return all(re.search(constraint, trace_str) for constraint in self.constraints) @@ -107,9 +119,18 @@ def minimal_expl(self, trace): :param trace: A Trace instance. :return: Explanation of why the trace is non-conformant. """ + + # Because constraints that are not activated should not be considered we create a new explainer with the relevant constraints in this case + activation = self.activation(trace) + if any(value == 0 for value in activation): + new_explainer = Explainer() + for idx, value in enumerate(activation): + if value == 1: + new_explainer.add_constraint(self.constraints[idx]) + return new_explainer.minimal_expl(trace) + if self.conformant(trace): return "The trace is already conformant, no changes needed." - explanations = None for constraint in self.constraints: @@ -125,6 +146,15 @@ def minimal_expl(self, trace): return "Trace is non-conformant, but the specific constraint violation could not be determined." def counterfactual_expl(self, trace): + + activation = self.activation(trace) + if any(value == 0 for value in activation): + new_explainer = Explainer() + for idx, value in enumerate(activation): + if value == 1: + new_explainer.add_constraint(self.constraints[idx]) + return new_explainer.counterfactual_expl(trace) + if self.conformant(trace): return "The trace is already conformant, no changes needed." score = self.evaluate_similarity(trace) @@ -262,3 +292,12 @@ def levenshtein_distance(seq1, seq2): ) return matrix[size_x-1][size_y-1] +trace = Trace(['A', 'C', 'F']) + +exp = Explainer() + +exp.add_constraint(".*AC.*") +exp.add_constraint("XY") + +print(exp.conformant(trace)) +print(exp.counterfactual_expl(trace)) \ No newline at end of file From 5b1b525c325cc893d75dd74450909a50c8c13c47 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 14 Mar 2024 08:41:07 +0100 Subject: [PATCH 07/54] Made some changes to the activation and logic --- explainer/explainer.py | 183 +++++++++++++++++++++++++++--- tests/explainer/explainer_test.py | 29 ++++- 2 files changed, 190 insertions(+), 22 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 43c8a23..ede6875 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -1,6 +1,7 @@ import itertools +import math import re -from itertools import combinations, permutations, product +from itertools import combinations, permutations, product,combinations_with_replacement, chain class Trace: def __init__(self, nodes): @@ -28,6 +29,57 @@ def __split__(self): for node in self.nodes: spl.append(node) return spl +class EventLog: + def __init__(self, traces=None): + """ + Initializes an EventLog instance. + + :param traces: A list of Trace instances. + """ + self.log = {} + if traces: + for trace in traces: + self.add_trace(trace) + + def add_trace(self, trace, count = 1): + """ + Adds a trace to the log or increments its count if it already exists. + + :param trace: A Trace instance to add. + """ + trace_tuple = tuple(trace.nodes) + if trace_tuple in self.log: + self.log[trace_tuple] += count + else: + self.log[trace_tuple] = count + + def remove_trace(self, trace, count = 1): + """ + Removes a trace from the log or decrements its count if the count is greater than 1. + + :param trace: A Trace instance to remove. + """ + trace_tuple = tuple(trace.nodes) + if trace_tuple in self.log: + if self.log[trace_tuple] > count: + self.log[trace_tuple] -= count + else: + del self.log[trace_tuple] + def __str__(self): + return str(self.log) + def __len__(self): + """ + Returns the total number of trace occurrences in the log. + """ + return sum(self.log.values()) + + def __iter__(self): + """ + Allows iteration over each trace occurrence in the log. + """ + for trace_tuple, count in self.log.items(): + for _ in range(count): + yield Trace(list(trace_tuple)) class Explainer: def __init__(self): @@ -75,14 +127,40 @@ def activation(self, trace): :return: Boolean indicating if any constraint is activated. """ con_activation = [0] * len(self.constraints) + activated = False for idx, con in enumerate(self.constraints): + if activated: + activated = False + continue + target = self.identify_existance_constraints(con) + if target: + con_activation[idx] = 1 + continue for event in trace.nodes: - if event in con: - con_activation[idx] = 1 - continue + if event in con: + con_activation[idx] = 1 + activated = True + break return con_activation - def conformant(self, trace): + def identify_existance_constraints(self, pattern): + + # Check for AtLeastOne constraint + for match in re.finditer(r'(? 100: return f'{explanation}\n Maximum depth of {depth -1} reached' score = self.evaluate_similarity(working_trace) @@ -184,7 +264,6 @@ def operate_on_trace(self, trace, score, explanation_path, depth = 0): explanation = subtrace[1] if best_subtrace == None: for subtrace in counter_factuals: - print(subtrace[0].nodes) self.operate_on_trace(subtrace[0], score, explanation_path, depth + 1) explanation_string = explanation_path + '\n' + str(explanation) return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) @@ -236,6 +315,34 @@ def modify_subtrace(self, trace): return potential_subtraces + def determine_shapley_value(self, log, constraints, index): + """Determines the Shapley value-based contribution of a constraint to a the + overall conformance rate. + Args: + log (dictionary): The event log, where keys are strings and values are + ints + constraints (list): A list of constraints (regexp strings) + index (int): The + Returns: + float: The contribution of the constraint to the overall conformance + rate + """ + if len(constraints) < index: + raise Exception ( + 'Constraint not in constraint list.') + contributor = constraints[index] + sub_ctrbs = [] + reduced_constraints = [c for c in constraints if not c == contributor] + subsets = determine_powerset(reduced_constraints) + for subset in subsets: + lsubset = list(subset) + constraints_without = [c for c in constraints if c in lsubset] + constraints_with = [c for c in constraints if c in lsubset + [contributor]] + weight = (math.factorial(len(lsubset)) * math.factorial(len(constraints)-1-len(lsubset)))/math.factorial(len(constraints)) + sub_ctrb = weight * (self.determine_conformance_rate(log, constraints_without) - self.determine_conformance_rate(log, constraints_with)) + sub_ctrbs.append(sub_ctrb) + return sum(sub_ctrbs) + def evaluate_similarity(self, trace): length = len(self.adherent_trace) trace_len = len("".join(trace)) @@ -243,6 +350,27 @@ def evaluate_similarity(self, trace): max_distance = max(length, trace_len) normalized_score = 1 - lev_distance / max_distance return normalized_score + + def determine_conformance_rate(self, event_log, constraints = None): + if not self.constraints and not constraints: + return "The explainer have no constraints" + conformant = 0 + for trace in event_log: + if self.conformant(trace, constraints): + conformant += 1 + return round(conformant / len(event_log), 2) + + +def determine_powerset(elements): + """Determines the powerset of a list of elements + Args: + elements (set): Set of elements + Returns: + list: Powerset of elements + """ + lset = list(elements) + ps_elements = chain.from_iterable(combinations(lset, option) for option in range(len(lset) + 1)) + return [set(ps_element) for ps_element in ps_elements] def get_sublists(lst): """ @@ -268,6 +396,7 @@ def get_iterative_subtrace(trace): sublists.append(trace.nodes[0:i+1]) return sublists + def levenshtein_distance(seq1, seq2): """ Calculates the Levenshtein distance between two sequences. @@ -292,12 +421,30 @@ def levenshtein_distance(seq1, seq2): ) return matrix[size_x-1][size_y-1] -trace = Trace(['A', 'C', 'F']) - +#event_log = EventLog() exp = Explainer() - -exp.add_constraint(".*AC.*") -exp.add_constraint("XY") - -print(exp.conformant(trace)) -print(exp.counterfactual_expl(trace)) \ No newline at end of file +exp.add_constraint('^A') +exp.add_constraint('C$') +print(exp.conformant(Trace(['AXC']))) +#trace1 = Trace(['A','B','C']) +#trace2 = Trace(['B', 'C']) +#trace3 = Trace(['A', 'B']) +#trace4 = Trace(['B']) +# +## Adding traces +#event_log.add_trace(trace1, 5) +#event_log.add_trace(trace2, 10) +#event_log.add_trace(trace3, 5) +#event_log.add_trace(trace4, 5) +#for con in exp.constraints: +# print(f"existance constraints: {exp.identify_existance_constraints(con)}") +##print(event_log) +##print(exp.determine_conformance_rate(event_log)) +#print("False: " + str(exp.conformant(trace2))+ " activated: " + str(exp.activation(trace2))) +#print("False: " + str(exp.conformant(trace3))+ " activated: " + str(exp.activation(trace3))) +#print("False: " + str(exp.conformant(trace4))+ " activated: " + str(exp.activation(trace4))) +#print("True: " + str(exp.conformant(trace1))+ " activated: " + str(exp.activation(trace1))) +# +# +#print('Contribution ^a:', exp.determine_shapley_value(event_log, exp.constraints, 0)) +#print('Contribution c$:', exp.determine_shapley_value(event_log, exp.constraints, 1)) \ No newline at end of file diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index b480532..823b32b 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -77,8 +77,8 @@ def test_complex_regex_constraint(): def test_constraint_not_covered(): trace = Trace(['A', 'B', 'C']) explainer = Explainer() - explainer.add_constraint('D') # This node 'D' does not exist in the trace - assert not explainer.activation(trace), "The constraint should not be activated by the trace." + explainer.add_constraint('D*') # This node 'D' does not exist in the trace + assert explainer.activation(trace) == [0], "The constraint should not be activated by the trace." # Test 12: Empty trace and constraints def test_empty_trace_and_constraints(): @@ -143,7 +143,7 @@ def test_explaination(): assert explainer.conformant(non_conformant_trace) == False assert explainer.conformant(conformant_trace) == True assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')" - assert explainer.counterfactual_expl(non_conformant_trace) == "Suggested change to make the trace (['A', 'C']) conformant: Addition: A->B->C" + assert explainer.counterfactual_expl(non_conformant_trace) == "\nAddition (Added B at position 1): A->B->C" # Test 19: Complex explaination test. """ This part is not very complex as of now and is very much up for change, the complexity of counterfactuals @@ -158,4 +158,25 @@ def test_complex_counterfactual_explanation(): counterfactual_explanation = explainer.counterfactual_expl(non_conformant_trace) - assert counterfactual_explanation == "Suggested change to make the trace (['A', 'C', 'E', 'D']) conformant: Addition: A->B->C->E->D" \ No newline at end of file + assert counterfactual_explanation == "\nAddition (Added B at position 1): A->B->C->E->D" + +# Test 20: Event logs +def test_event_log(): + event_log = EventLog() + assert event_log != None + trace = Trace(['A', 'B', 'C']) + event_log.add_trace(trace) + assert event_log.log == {('A', 'B', 'C'): 1} # There should be one instance of the trace in the log + event_log.add_trace(trace, 5) + assert event_log.log == {('A', 'B', 'C'): 6} # There should be 6 instances of the trace in the log + event_log.remove_trace(trace) + assert event_log.log == {('A', 'B', 'C'): 5} # There should be 5 instances of the trace + event_log.remove_trace(trace, 5) + assert event_log.log == {} # The log should be emptied + event_log.add_trace(trace, 5) + event_log.remove_trace(trace, 10) + assert event_log.log == {} # The log should be emptied + trace2 = Trace(['X', 'Y', 'Z']) + event_log.add_trace(trace, 5) + event_log.add_trace(trace2, 7) + assert event_log.log == {('A', 'B', 'C'): 5,('X', 'Y', 'Z'): 7} # There should be several traces in the log From d174146c5830450d0eab656c7da71e504a25b8c7 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 14 Mar 2024 08:41:19 +0100 Subject: [PATCH 08/54] Updated notebook --- explainer/tutorial/explainer_tutorial.ipynb | 121 +++++++++++++++----- 1 file changed, 95 insertions(+), 26 deletions(-) diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index 6cb27c0..416863c 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -22,7 +22,15 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n" + ] + } + ], "source": [ "import sys\n", "sys.path.append('../')\n", @@ -95,40 +103,31 @@ "text": [ "Constraint: A.*B.*C\n", "Trace:['A', 'C']\n", - "['A', 'C']\n", + "\n", "Addition (Added B at position 1): A->B->C\n", - "['A', 'B', 'C']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['C', 'B', 'A']\n", - "['C', 'B', 'A']\n", + "\n", "Addition (Added A at position 1): C->A->B->A\n", "Subtraction (Removed C from position 0): A->B->A\n", "Addition (Added C at position 2): A->B->C->A\n", - "['A', 'B', 'C', 'A']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'A', 'C']\n", - "['A', 'A', 'C']\n", + "\n", "Addition (Added B at position 2): A->A->B->C\n", - "['A', 'A', 'B', 'C']\n", "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", - "['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", - "Subtraction (Removed TEST from position 4): A->A->C->A->A->C->X->Y\n", - "Addition (Added B at position 2): A->A->B->C->A->A->C->X->Y\n", - "['A', 'A', 'B', 'C', 'A', 'A', 'C', 'X', 'Y']\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", "-----------\n", "Constraint: AC\n", "Trace:['A', 'X', 'C']\n", - "['A', 'X', 'C']\n", + "\n", "Subtraction (Removed X from position 1): A->C\n", - "['A', 'C']\n", "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", "-----------\n", "constraint: AC\n", @@ -136,14 +135,11 @@ "constraint: A.*B.*C.*\n", "constraint: A.*D.*B*\n", "constraint: A[^D]*B\n", + "constraint: B.*[^X].*\n", "Trace:['A', 'X', 'C']\n", - "['A', 'X', 'C']\n", - "Addition (Added C at position 1): A->C->X->C\n", - "Addition (Added B at position 0): B->A->C->X->C\n", - "Addition (Added B at position 3): B->A->C->B->X->C\n", - "Addition (Added D at position 4): B->A->C->B->D->X->C\n", - "['B', 'A', 'C', 'B', 'D', 'X', 'C']\n", - "Non-conformance due to: Constraint (A[^D]*B) is violated by subtrace: ('A', 'X')\n" + "\n", + "Subtraction (Removed X from position 1): A->C\n", + "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n" ] } ], @@ -173,8 +169,8 @@ "print('-----------')\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", - "print(explainer.minimal_expl(non_conformant_trace))\n", + "#print(explainer.counterfactual_expl(non_conformant_trace))\n", + "#print(explainer.minimal_expl(non_conformant_trace))\n", "\n", "\n", "explainer.remove_constraint(0)\n", @@ -185,13 +181,14 @@ "print('Trace:' + str(non_conformant_trace.nodes))\n", "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", + "print('-----------')\n", "\n", "explainer.add_constraint('B.*A.*B.*C')\n", "explainer.add_constraint('A.*B.*C.*')\n", "explainer.add_constraint('A.*D.*B*')\n", "explainer.add_constraint('A[^D]*B')\n", + "explainer.add_constraint('B.*[^X].*')\n", "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", - "print('-----------')\n", "for con in explainer.constraints:\n", " print(f'constraint: {con}')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", @@ -205,7 +202,79 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 5: Signal (Not implemented yet)" + "## Step 5: Event Logs and Shapely values\n", + "\n", + "The event logs in this context is built with traces, here's how you set them up." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conformance rate: 0.2\n", + "Contribution C$: -0.09999999999999998\n", + "Contribution ^A: 0.09999999999999998\n" + ] + } + ], + "source": [ + "from explainer import EventLog\n", + "\n", + "event_log = EventLog()\n", + "trace1 = Trace(['A', 'B', 'C'])\n", + "trace2 = Trace(['B', 'C'])\n", + "trace3 = Trace(['A', 'B'])\n", + "trace4 = Trace(['B'])\n", + "\n", + "event_log.add_trace(trace1, 5) # The second is how many traces you'd like to add, leave blank for 1\n", + "event_log.add_trace(trace2, 10)\n", + "event_log.add_trace(trace3, 5)\n", + "event_log.add_trace(trace4, 5)\n", + "\n", + "\n", + "exp = Explainer()\n", + "exp.add_constraint(\"C$\")\n", + "exp.add_constraint(\"^A\")\n", + "print(\"Conformance rate: \"+ str(exp.determine_conformance_rate(event_log)))\n", + "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", + "print('Contribution ^A:', exp.determine_shapley_value(event_log, exp.constraints, 1))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exp = Explainer()\n", + "event_log = EventLog()\n", + "trace1 = Trace(['A', 'B', 'C'])\n", + "trace2 = Trace(['B', 'C'])\n", + "trace3 = Trace(['A', 'B'])\n", + "trace4 = Trace(['B'])\n", + "trace5 = Trace(['A', 'C'])\n", + "\n", + "\n", + "event_log.add_trace(trace1, 5) # The second is how many traces you'd like to add, leave blank for 1\n", + "event_log.add_trace(trace2, 10)\n", + "event_log.add_trace(trace3, 5)\n", + "event_log.add_trace(trace4, 5)\n", + "\n", + "\n", + "exp = Explainer()\n", + "exp.add_constraint(\"C$\")\n", + "exp.add_constraint(\"^A\")\n", + "exp.add_constraint(\"B*\")\n", + "print(\"Conformance rate: \"+ str(exp.determine_conformance_rate(event_log)))\n", + "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", + "print('Contribution ^A:', exp.determine_shapley_value(event_log, exp.constraints, 1))\n", + "print('Contribution B*:', exp.determine_shapley_value(event_log, exp.constraints, 2))\n", + "\n" ] } ], @@ -225,7 +294,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.10.12" } }, "nbformat": 4, From 17889ce81239e3359c39713fb4c54a72910ddc2b Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 14 Mar 2024 09:20:04 +0100 Subject: [PATCH 09/54] some small logic changes --- explainer/explainer.py | 2 +- explainer/tutorial/explainer_tutorial.ipynb | 49 ++++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index ede6875..7cfc0c8 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -136,7 +136,7 @@ def activation(self, trace): if target: con_activation[idx] = 1 continue - for event in trace.nodes: + for event in con: if event in con: con_activation[idx] = 1 activated = True diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index 416863c..e2be6de 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -20,17 +20,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 7, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n" - ] - } - ], + "outputs": [], "source": [ "import sys\n", "sys.path.append('../')\n", @@ -47,7 +39,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -66,7 +58,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -94,7 +86,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -209,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -247,9 +239,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "error", + "evalue": "multiple repeat at position 2", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31merror\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[12], line 20\u001b[0m\n\u001b[1;32m 18\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mC$\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 19\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m^A\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 20\u001b[0m \u001b[43mexp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd_constraint\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mB*\u001b[39;49m\u001b[38;5;132;43;01m{1}\u001b[39;49;00m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 21\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mconformant AC :\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(exp\u001b[38;5;241m.\u001b[39mconformant(trace5)))\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConformance rate: \u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(exp\u001b[38;5;241m.\u001b[39mdetermine_conformance_rate(event_log)))\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer.py:99\u001b[0m, in \u001b[0;36mExplainer.add_constraint\u001b[0;34m(self, regex)\u001b[0m\n\u001b[1;32m 93\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 94\u001b[0m \u001b[38;5;124;03mAdds a new constraint and updates the nodes list.\u001b[39;00m\n\u001b[1;32m 95\u001b[0m \u001b[38;5;124;03m\u001b[39;00m\n\u001b[1;32m 96\u001b[0m \u001b[38;5;124;03m:param regex: A regular expression representing the constraint.\u001b[39;00m\n\u001b[1;32m 97\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 98\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconstraints\u001b[38;5;241m.\u001b[39mappend(regex)\n\u001b[0;32m---> 99\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcontradiction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 100\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconstraints\u001b[38;5;241m.\u001b[39mremove(regex)\n\u001b[1;32m 101\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConstraint \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mregex\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m contradicts the other constraints.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer.py:189\u001b[0m, in \u001b[0;36mExplainer.contradiction\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 187\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m combination \u001b[38;5;129;01min\u001b[39;00m product(nodes, repeat\u001b[38;5;241m=\u001b[39mlength):\n\u001b[1;32m 188\u001b[0m test_str \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(combination)\n\u001b[0;32m--> 189\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28;43mall\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mre\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msearch\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcon\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtest_str\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcon\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconstraints\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 190\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39madherent_trace \u001b[38;5;241m=\u001b[39m test_str\n\u001b[1;32m 191\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mFalse\u001b[39;00m \u001b[38;5;66;03m# Found a match\u001b[39;00m\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer.py:189\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 187\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m combination \u001b[38;5;129;01min\u001b[39;00m product(nodes, repeat\u001b[38;5;241m=\u001b[39mlength):\n\u001b[1;32m 188\u001b[0m test_str \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(combination)\n\u001b[0;32m--> 189\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mall\u001b[39m(\u001b[43mre\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msearch\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcon\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtest_str\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m con \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconstraints):\n\u001b[1;32m 190\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39madherent_trace \u001b[38;5;241m=\u001b[39m test_str\n\u001b[1;32m 191\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mFalse\u001b[39;00m \u001b[38;5;66;03m# Found a match\u001b[39;00m\n", + "File \u001b[0;32m/usr/lib/python3.10/re.py:200\u001b[0m, in \u001b[0;36msearch\u001b[0;34m(pattern, string, flags)\u001b[0m\n\u001b[1;32m 197\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21msearch\u001b[39m(pattern, string, flags\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m):\n\u001b[1;32m 198\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Scan through string looking for a match to the pattern, returning\u001b[39;00m\n\u001b[1;32m 199\u001b[0m \u001b[38;5;124;03m a Match object, or None if no match was found.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 200\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_compile\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpattern\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39msearch(string)\n", + "File \u001b[0;32m/usr/lib/python3.10/re.py:303\u001b[0m, in \u001b[0;36m_compile\u001b[0;34m(pattern, flags)\u001b[0m\n\u001b[1;32m 301\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m sre_compile\u001b[38;5;241m.\u001b[39misstring(pattern):\n\u001b[1;32m 302\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfirst argument must be string or compiled pattern\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 303\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43msre_compile\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcompile\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpattern\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 304\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (flags \u001b[38;5;241m&\u001b[39m DEBUG):\n\u001b[1;32m 305\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(_cache) \u001b[38;5;241m>\u001b[39m\u001b[38;5;241m=\u001b[39m _MAXCACHE:\n\u001b[1;32m 306\u001b[0m \u001b[38;5;66;03m# Drop the oldest item\u001b[39;00m\n", + "File \u001b[0;32m/usr/lib/python3.10/sre_compile.py:788\u001b[0m, in \u001b[0;36mcompile\u001b[0;34m(p, flags)\u001b[0m\n\u001b[1;32m 786\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m isstring(p):\n\u001b[1;32m 787\u001b[0m pattern \u001b[38;5;241m=\u001b[39m p\n\u001b[0;32m--> 788\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43msre_parse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparse\u001b[49m\u001b[43m(\u001b[49m\u001b[43mp\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 789\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 790\u001b[0m pattern \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "File \u001b[0;32m/usr/lib/python3.10/sre_parse.py:955\u001b[0m, in \u001b[0;36mparse\u001b[0;34m(str, flags, state)\u001b[0m\n\u001b[1;32m 952\u001b[0m state\u001b[38;5;241m.\u001b[39mstr \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m\n\u001b[1;32m 954\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 955\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43m_parse_sub\u001b[49m\u001b[43m(\u001b[49m\u001b[43msource\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m&\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mSRE_FLAG_VERBOSE\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 956\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m Verbose:\n\u001b[1;32m 957\u001b[0m \u001b[38;5;66;03m# the VERBOSE flag was switched on inside the pattern. to be\u001b[39;00m\n\u001b[1;32m 958\u001b[0m \u001b[38;5;66;03m# on the safe side, we'll parse the whole thing again...\u001b[39;00m\n\u001b[1;32m 959\u001b[0m state \u001b[38;5;241m=\u001b[39m State()\n", + "File \u001b[0;32m/usr/lib/python3.10/sre_parse.py:444\u001b[0m, in \u001b[0;36m_parse_sub\u001b[0;34m(source, state, verbose, nested)\u001b[0m\n\u001b[1;32m 442\u001b[0m start \u001b[38;5;241m=\u001b[39m source\u001b[38;5;241m.\u001b[39mtell()\n\u001b[1;32m 443\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m--> 444\u001b[0m itemsappend(\u001b[43m_parse\u001b[49m\u001b[43m(\u001b[49m\u001b[43msource\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mverbose\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnested\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 445\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnested\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mand\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mitems\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m 446\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m sourcematch(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m|\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[1;32m 447\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n", + "File \u001b[0;32m/usr/lib/python3.10/sre_parse.py:672\u001b[0m, in \u001b[0;36m_parse\u001b[0;34m(source, state, verbose, nested, first)\u001b[0m\n\u001b[1;32m 669\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m source\u001b[38;5;241m.\u001b[39merror(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnothing to repeat\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 670\u001b[0m source\u001b[38;5;241m.\u001b[39mtell() \u001b[38;5;241m-\u001b[39m here \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mlen\u001b[39m(this))\n\u001b[1;32m 671\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m item[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m0\u001b[39m] \u001b[38;5;129;01min\u001b[39;00m _REPEATCODES:\n\u001b[0;32m--> 672\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m source\u001b[38;5;241m.\u001b[39merror(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmultiple repeat\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 673\u001b[0m source\u001b[38;5;241m.\u001b[39mtell() \u001b[38;5;241m-\u001b[39m here \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mlen\u001b[39m(this))\n\u001b[1;32m 674\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m item[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m0\u001b[39m] \u001b[38;5;129;01mis\u001b[39;00m SUBPATTERN:\n\u001b[1;32m 675\u001b[0m group, add_flags, del_flags, p \u001b[38;5;241m=\u001b[39m item[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m1\u001b[39m]\n", + "\u001b[0;31merror\u001b[0m: multiple repeat at position 2" + ] + } + ], "source": [ "exp = Explainer()\n", "event_log = EventLog()\n", @@ -264,12 +277,14 @@ "event_log.add_trace(trace2, 10)\n", "event_log.add_trace(trace3, 5)\n", "event_log.add_trace(trace4, 5)\n", + "event_log.add_trace(trace5, 10)\n", "\n", "\n", "exp = Explainer()\n", "exp.add_constraint(\"C$\")\n", "exp.add_constraint(\"^A\")\n", - "exp.add_constraint(\"B*\")\n", + "exp.add_constraint(\"B*\\{1\\}\")\n", + "print(\"conformant AC :\" + str(exp.conformant(trace5)))\n", "print(\"Conformance rate: \"+ str(exp.determine_conformance_rate(event_log)))\n", "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", "print('Contribution ^A:', exp.determine_shapley_value(event_log, exp.constraints, 1))\n", From bf00dc266d49e8987cce26f1e2a59eeb52875c96 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 14 Mar 2024 13:10:31 +0100 Subject: [PATCH 10/54] Notebook compile --- explainer/explainer.py | 32 ++++++++++++++--- explainer/tutorial/explainer_tutorial.ipynb | 39 ++++++++------------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 7cfc0c8..226075d 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -178,7 +178,6 @@ def conformant(self, trace, constraints = None): if constraints: return all(re.search(constraint, trace_str) for constraint in constraints) return all(re.search(constraint, trace_str) for constraint in self.constraints) - def contradiction(self): nodes = self.get_nodes_from_constraint() max_length = 10 # Set a reasonable max length to avoid infinite loops @@ -190,6 +189,29 @@ def contradiction(self): self.adherent_trace = test_str return False # Found a match return True # No combination satisfied all constraints + """ + def contradiction(self): + nodes = self.get_nodes_from_constraint() + nodes = [] + nodes + nodes + event_str = "" + match_found = False + for event in nodes: + event_str = event + if all(re.search(con, event_str) for con in self.constraints): + self.adherent_trace = event_str + match_found = True + break + for event2 in nodes: + if (str(event) != str(event2)): + event_str += event2 + if all(re.search(con, event_str) for con in self.constraints): + self.adherent_trace = event_str + match_found = True + break + + return not match_found + """ + def minimal_expl(self, trace): @@ -422,10 +444,10 @@ def levenshtein_distance(seq1, seq2): return matrix[size_x-1][size_y-1] #event_log = EventLog() -exp = Explainer() -exp.add_constraint('^A') -exp.add_constraint('C$') -print(exp.conformant(Trace(['AXC']))) +#exp = Explainer() +#exp.add_constraint('^A') +#exp.add_constraint('ABC') +#print(exp.conformant(Trace(['AXC']))) #trace1 = Trace(['A','B','C']) #trace2 = Trace(['B', 'C']) #trace3 = Trace(['A', 'B']) diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index e2be6de..cb283ef 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -39,7 +39,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -58,7 +58,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -86,7 +86,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -201,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -239,27 +239,18 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "metadata": {}, "outputs": [ { - "ename": "error", - "evalue": "multiple repeat at position 2", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31merror\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[12], line 20\u001b[0m\n\u001b[1;32m 18\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mC$\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 19\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m^A\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 20\u001b[0m \u001b[43mexp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd_constraint\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mB*\u001b[39;49m\u001b[38;5;132;43;01m{1}\u001b[39;49;00m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 21\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mconformant AC :\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(exp\u001b[38;5;241m.\u001b[39mconformant(trace5)))\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConformance rate: \u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(exp\u001b[38;5;241m.\u001b[39mdetermine_conformance_rate(event_log)))\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer.py:99\u001b[0m, in \u001b[0;36mExplainer.add_constraint\u001b[0;34m(self, regex)\u001b[0m\n\u001b[1;32m 93\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 94\u001b[0m \u001b[38;5;124;03mAdds a new constraint and updates the nodes list.\u001b[39;00m\n\u001b[1;32m 95\u001b[0m \u001b[38;5;124;03m\u001b[39;00m\n\u001b[1;32m 96\u001b[0m \u001b[38;5;124;03m:param regex: A regular expression representing the constraint.\u001b[39;00m\n\u001b[1;32m 97\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 98\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconstraints\u001b[38;5;241m.\u001b[39mappend(regex)\n\u001b[0;32m---> 99\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcontradiction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 100\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconstraints\u001b[38;5;241m.\u001b[39mremove(regex)\n\u001b[1;32m 101\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConstraint \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mregex\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m contradicts the other constraints.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer.py:189\u001b[0m, in \u001b[0;36mExplainer.contradiction\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 187\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m combination \u001b[38;5;129;01min\u001b[39;00m product(nodes, repeat\u001b[38;5;241m=\u001b[39mlength):\n\u001b[1;32m 188\u001b[0m test_str \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(combination)\n\u001b[0;32m--> 189\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28;43mall\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mre\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msearch\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcon\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtest_str\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcon\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconstraints\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 190\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39madherent_trace \u001b[38;5;241m=\u001b[39m test_str\n\u001b[1;32m 191\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mFalse\u001b[39;00m \u001b[38;5;66;03m# Found a match\u001b[39;00m\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer.py:189\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 187\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m combination \u001b[38;5;129;01min\u001b[39;00m product(nodes, repeat\u001b[38;5;241m=\u001b[39mlength):\n\u001b[1;32m 188\u001b[0m test_str \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(combination)\n\u001b[0;32m--> 189\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mall\u001b[39m(\u001b[43mre\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msearch\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcon\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtest_str\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m con \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconstraints):\n\u001b[1;32m 190\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39madherent_trace \u001b[38;5;241m=\u001b[39m test_str\n\u001b[1;32m 191\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mFalse\u001b[39;00m \u001b[38;5;66;03m# Found a match\u001b[39;00m\n", - "File \u001b[0;32m/usr/lib/python3.10/re.py:200\u001b[0m, in \u001b[0;36msearch\u001b[0;34m(pattern, string, flags)\u001b[0m\n\u001b[1;32m 197\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21msearch\u001b[39m(pattern, string, flags\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m):\n\u001b[1;32m 198\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Scan through string looking for a match to the pattern, returning\u001b[39;00m\n\u001b[1;32m 199\u001b[0m \u001b[38;5;124;03m a Match object, or None if no match was found.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 200\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_compile\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpattern\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39msearch(string)\n", - "File \u001b[0;32m/usr/lib/python3.10/re.py:303\u001b[0m, in \u001b[0;36m_compile\u001b[0;34m(pattern, flags)\u001b[0m\n\u001b[1;32m 301\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m sre_compile\u001b[38;5;241m.\u001b[39misstring(pattern):\n\u001b[1;32m 302\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfirst argument must be string or compiled pattern\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 303\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43msre_compile\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcompile\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpattern\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 304\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (flags \u001b[38;5;241m&\u001b[39m DEBUG):\n\u001b[1;32m 305\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(_cache) \u001b[38;5;241m>\u001b[39m\u001b[38;5;241m=\u001b[39m _MAXCACHE:\n\u001b[1;32m 306\u001b[0m \u001b[38;5;66;03m# Drop the oldest item\u001b[39;00m\n", - "File \u001b[0;32m/usr/lib/python3.10/sre_compile.py:788\u001b[0m, in \u001b[0;36mcompile\u001b[0;34m(p, flags)\u001b[0m\n\u001b[1;32m 786\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m isstring(p):\n\u001b[1;32m 787\u001b[0m pattern \u001b[38;5;241m=\u001b[39m p\n\u001b[0;32m--> 788\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43msre_parse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparse\u001b[49m\u001b[43m(\u001b[49m\u001b[43mp\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 789\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 790\u001b[0m pattern \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", - "File \u001b[0;32m/usr/lib/python3.10/sre_parse.py:955\u001b[0m, in \u001b[0;36mparse\u001b[0;34m(str, flags, state)\u001b[0m\n\u001b[1;32m 952\u001b[0m state\u001b[38;5;241m.\u001b[39mstr \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m\n\u001b[1;32m 954\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 955\u001b[0m p \u001b[38;5;241m=\u001b[39m \u001b[43m_parse_sub\u001b[49m\u001b[43m(\u001b[49m\u001b[43msource\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m&\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mSRE_FLAG_VERBOSE\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 956\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m Verbose:\n\u001b[1;32m 957\u001b[0m \u001b[38;5;66;03m# the VERBOSE flag was switched on inside the pattern. to be\u001b[39;00m\n\u001b[1;32m 958\u001b[0m \u001b[38;5;66;03m# on the safe side, we'll parse the whole thing again...\u001b[39;00m\n\u001b[1;32m 959\u001b[0m state \u001b[38;5;241m=\u001b[39m State()\n", - "File \u001b[0;32m/usr/lib/python3.10/sre_parse.py:444\u001b[0m, in \u001b[0;36m_parse_sub\u001b[0;34m(source, state, verbose, nested)\u001b[0m\n\u001b[1;32m 442\u001b[0m start \u001b[38;5;241m=\u001b[39m source\u001b[38;5;241m.\u001b[39mtell()\n\u001b[1;32m 443\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m--> 444\u001b[0m itemsappend(\u001b[43m_parse\u001b[49m\u001b[43m(\u001b[49m\u001b[43msource\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstate\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mverbose\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnested\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 445\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnested\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mand\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mitems\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m 446\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m sourcematch(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m|\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[1;32m 447\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n", - "File \u001b[0;32m/usr/lib/python3.10/sre_parse.py:672\u001b[0m, in \u001b[0;36m_parse\u001b[0;34m(source, state, verbose, nested, first)\u001b[0m\n\u001b[1;32m 669\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m source\u001b[38;5;241m.\u001b[39merror(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnothing to repeat\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 670\u001b[0m source\u001b[38;5;241m.\u001b[39mtell() \u001b[38;5;241m-\u001b[39m here \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mlen\u001b[39m(this))\n\u001b[1;32m 671\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m item[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m0\u001b[39m] \u001b[38;5;129;01min\u001b[39;00m _REPEATCODES:\n\u001b[0;32m--> 672\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m source\u001b[38;5;241m.\u001b[39merror(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmultiple repeat\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 673\u001b[0m source\u001b[38;5;241m.\u001b[39mtell() \u001b[38;5;241m-\u001b[39m here \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mlen\u001b[39m(this))\n\u001b[1;32m 674\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m item[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m0\u001b[39m] \u001b[38;5;129;01mis\u001b[39;00m SUBPATTERN:\n\u001b[1;32m 675\u001b[0m group, add_flags, del_flags, p \u001b[38;5;241m=\u001b[39m item[\u001b[38;5;241m0\u001b[39m][\u001b[38;5;241m1\u001b[39m]\n", - "\u001b[0;31merror\u001b[0m: multiple repeat at position 2" + "name": "stdout", + "output_type": "stream", + "text": [ + "conformant AC :False\n", + "Conformance rate: 0.14\n", + "Contribution C$: -0.07\n", + "Contribution ^A: 0.06999999999999999\n", + "Contribution B*: 0.0\n" ] } ], @@ -283,7 +274,7 @@ "exp = Explainer()\n", "exp.add_constraint(\"C$\")\n", "exp.add_constraint(\"^A\")\n", - "exp.add_constraint(\"B*\\{1\\}\")\n", + "exp.add_constraint(\"B+\")\n", "print(\"conformant AC :\" + str(exp.conformant(trace5)))\n", "print(\"Conformance rate: \"+ str(exp.determine_conformance_rate(event_log)))\n", "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", From a2b30168adfc9271944e5a9f80216d8e805a25e8 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 18 Mar 2024 22:41:43 +0100 Subject: [PATCH 11/54] added shapley_values --- explainer/explainer.py | 92 +++++++++++++++++---- explainer/tutorial/explainer_tutorial.ipynb | 39 +++++---- 2 files changed, 99 insertions(+), 32 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 226075d..1c6e2b6 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -30,16 +30,15 @@ def __split__(self): spl.append(node) return spl class EventLog: - def __init__(self, traces=None): + def __init__(self, trace=None): """ Initializes an EventLog instance. :param traces: A list of Trace instances. """ self.log = {} - if traces: - for trace in traces: - self.add_trace(trace) + if trace: + self.add_trace(trace) def add_trace(self, trace, count = 1): """ @@ -119,16 +118,18 @@ def remove_constraint(self, idx): if node not in remaining_nodes: self.nodes.discard(node) - def activation(self, trace): + def activation(self, trace, constraints = None): """ Checks if any of the nodes in the trace activates any constraint. :param trace: A Trace instance. :return: Boolean indicating if any constraint is activated. """ - con_activation = [0] * len(self.constraints) + if not constraints: + constraints = self.constraints + con_activation = [0] * len(constraints) activated = False - for idx, con in enumerate(self.constraints): + for idx, con in enumerate(constraints): if activated: activated = False continue @@ -136,7 +137,7 @@ def activation(self, trace): if target: con_activation[idx] = 1 continue - for event in con: + for event in trace: if event in con: con_activation[idx] = 1 activated = True @@ -167,7 +168,7 @@ def conformant(self, trace, constraints = None): :param trace: A Trace instance. :return: Boolean indicating if the trace is conformant with all constraints. """ - activation = self.activation(trace) + activation = self.activation(trace, constraints) if any(value == 0 for value in activation): new_explainer = Explainer() for idx, value in enumerate(activation): @@ -361,9 +362,46 @@ def determine_shapley_value(self, log, constraints, index): constraints_without = [c for c in constraints if c in lsubset] constraints_with = [c for c in constraints if c in lsubset + [contributor]] weight = (math.factorial(len(lsubset)) * math.factorial(len(constraints)-1-len(lsubset)))/math.factorial(len(constraints)) - sub_ctrb = weight * (self.determine_conformance_rate(log, constraints_without) - self.determine_conformance_rate(log, constraints_with)) + sub_ctrb = weight * (self.determine_conformance_rate(log, constraints_without) - self.determine_conformance_rate(log, constraints_with)) sub_ctrbs.append(sub_ctrb) return sum(sub_ctrbs) + + def determine_shapley_values_traces(self, log): + """ + Determines the Shapley value-based contribution of each trace to the overall conformance rate. + + Args: + log (EventLog): The event log containing the traces. + + Returns: + dict: A dictionary with trace representations as keys and their Shapley values as values. + """ + shapley_values = {} + traces = [Trace(list(t)) for t in log.log.keys()] # Convert trace tuples to Trace instances + total_traces = len(traces) + + for trace in traces: + trace_contribution = 0 + for subset_size in range(total_traces): + for subset in combinations(traces, subset_size): + if trace in subset: + continue + subset_with_trace = list(subset) + [trace] + # Use the existing log to create a new EventLog with the subset of traces + log_without_trace = EventLog() + log_with_trace = EventLog() + for t in subset: + log_without_trace.add_trace(t, log.log[tuple(t.nodes)]) + log_with_trace.add_trace(trace, log.log[tuple(trace.nodes)]) + for t in subset: + log_with_trace.add_trace(t, log.log[tuple(t.nodes)]) + + weight = (math.factorial(len(subset)) * math.factorial(total_traces - len(subset) - 1)) / math.factorial(total_traces) + marginal_contribution = self.determine_conformance_rate(log_with_trace) - self.determine_conformance_rate(log_without_trace) + trace_contribution += weight * marginal_contribution + shapley_values[str(trace.nodes)] = trace_contribution + + return shapley_values def evaluate_similarity(self, trace): length = len(self.adherent_trace) @@ -376,11 +414,18 @@ def evaluate_similarity(self, trace): def determine_conformance_rate(self, event_log, constraints = None): if not self.constraints and not constraints: return "The explainer have no constraints" - conformant = 0 - for trace in event_log: - if self.conformant(trace, constraints): - conformant += 1 - return round(conformant / len(event_log), 2) + len_log = len(event_log) + if len_log == 0: + return 1 + non_conformant = 0 + if constraints == None: + constraints = self.constraints + for trace, count in event_log.log.items(): + for con in constraints: + if not re.search(con, "".join(trace)): + non_conformant += count + break + return (len_log - non_conformant) / len_log def determine_powerset(elements): @@ -444,8 +489,21 @@ def levenshtein_distance(seq1, seq2): return matrix[size_x-1][size_y-1] #event_log = EventLog() -#exp = Explainer() -#exp.add_constraint('^A') +exp = Explainer() +exp.add_constraint('^A') +exp.add_constraint('C$') +trace0 = Trace(['A', 'B', 'C']) +trace1 = Trace(['B', 'C']) +trace2 = Trace(['A','B']) +trace3 = Trace(['B']) +eventlog = EventLog() +eventlog.add_trace(trace0, 5) +eventlog.add_trace(trace1, 10) +eventlog.add_trace(trace2, 5) +eventlog.add_trace(trace3, 5) +print('Contribution ^a:', exp.determine_shapley_value(eventlog, exp.constraints, 0)) +print('Contribution c$:', exp.determine_shapley_value(eventlog, exp.constraints, 1)) +print(exp.determine_shapley_values_traces(eventlog)) #exp.add_constraint('ABC') #print(exp.conformant(Trace(['AXC']))) #trace1 = Trace(['A','B','C']) diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index cb283ef..ef25e03 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -22,7 +22,16 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution ^a: 0.5\n", + "Contribution c$: 0.30000000000000004\n" + ] + } + ], "source": [ "import sys\n", "sys.path.append('../')\n", @@ -209,8 +218,8 @@ "output_type": "stream", "text": [ "Conformance rate: 0.2\n", - "Contribution C$: -0.09999999999999998\n", - "Contribution ^A: 0.09999999999999998\n" + "Contribution ^A: 0.5\n", + "Contribution C$: 0.30000000000000004\n" ] } ], @@ -230,27 +239,27 @@ "\n", "\n", "exp = Explainer()\n", - "exp.add_constraint(\"C$\")\n", "exp.add_constraint(\"^A\")\n", + "exp.add_constraint(\"C$\")\n", "print(\"Conformance rate: \"+ str(exp.determine_conformance_rate(event_log)))\n", - "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", - "print('Contribution ^A:', exp.determine_shapley_value(event_log, exp.constraints, 1))\n" + "print('Contribution ^A:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", + "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 1))\n" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "conformant AC :False\n", + "conformant AC :True\n", "Conformance rate: 0.14\n", - "Contribution C$: -0.07\n", - "Contribution ^A: 0.06999999999999999\n", - "Contribution B*: 0.0\n" + "Contribution C$: 0.21\n", + "Contribution ^A: 0.36\n", + "Contribution B+: 0.29\n" ] } ], @@ -276,10 +285,10 @@ "exp.add_constraint(\"^A\")\n", "exp.add_constraint(\"B+\")\n", "print(\"conformant AC :\" + str(exp.conformant(trace5)))\n", - "print(\"Conformance rate: \"+ str(exp.determine_conformance_rate(event_log)))\n", - "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", - "print('Contribution ^A:', exp.determine_shapley_value(event_log, exp.constraints, 1))\n", - "print('Contribution B*:', exp.determine_shapley_value(event_log, exp.constraints, 2))\n", + "print(\"Conformance rate: \"+ str(round(exp.determine_conformance_rate(event_log), 2)))\n", + "print('Contribution C$:', round(exp.determine_shapley_value(event_log, exp.constraints, 0), 2))\n", + "print('Contribution ^A:', round(exp.determine_shapley_value(event_log, exp.constraints, 1), 2))\n", + "print('Contribution B+:', round(exp.determine_shapley_value(event_log, exp.constraints, 2), 2))\n", "\n" ] } From bd61c2f2a261e5261d6bacc9977c537a3eae0df4 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 21 Mar 2024 10:25:53 +0100 Subject: [PATCH 12/54] Progress on shapely for traces --- explainer/explainer.py | 113 +++++++------------- explainer/tutorial/explainer_tutorial.ipynb | 57 +++++++++- 2 files changed, 92 insertions(+), 78 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 1c6e2b6..1938f6d 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -64,6 +64,10 @@ def remove_trace(self, trace, count = 1): self.log[trace_tuple] -= count else: del self.log[trace_tuple] + def get_trace_cardinality(self, trace): + trace_tuple = tuple(trace.nodes) + if trace_tuple in self.log: + return self.log[trace_tuple] def __str__(self): return str(self.log) def __len__(self): @@ -190,30 +194,6 @@ def contradiction(self): self.adherent_trace = test_str return False # Found a match return True # No combination satisfied all constraints - """ - def contradiction(self): - nodes = self.get_nodes_from_constraint() - nodes = [] + nodes + nodes - event_str = "" - match_found = False - for event in nodes: - event_str = event - if all(re.search(con, event_str) for con in self.constraints): - self.adherent_trace = event_str - match_found = True - break - for event2 in nodes: - if (str(event) != str(event2)): - event_str += event2 - if all(re.search(con, event_str) for con in self.constraints): - self.adherent_trace = event_str - match_found = True - break - - return not match_found - """ - - def minimal_expl(self, trace): """ @@ -382,25 +362,27 @@ def determine_shapley_values_traces(self, log): for trace in traces: trace_contribution = 0 - for subset_size in range(total_traces): + for subset_size in range(total_traces + 1): for subset in combinations(traces, subset_size): - if trace in subset: - continue + weight = 1 / (total_traces * (total_traces - 1)) # Recalculate weight for each subset size subset_with_trace = list(subset) + [trace] - # Use the existing log to create a new EventLog with the subset of traces - log_without_trace = EventLog() - log_with_trace = EventLog() - for t in subset: - log_without_trace.add_trace(t, log.log[tuple(t.nodes)]) - log_with_trace.add_trace(trace, log.log[tuple(trace.nodes)]) - for t in subset: - log_with_trace.add_trace(t, log.log[tuple(t.nodes)]) - - weight = (math.factorial(len(subset)) * math.factorial(total_traces - len(subset) - 1)) / math.factorial(total_traces) - marginal_contribution = self.determine_conformance_rate(log_with_trace) - self.determine_conformance_rate(log_without_trace) - trace_contribution += weight * marginal_contribution + subset_without_trace = list(subset) + if trace not in subset: # Trace not in this subset + subset_with_trace = list(subset) + [trace] + subset_without_trace = list(subset) + log_with_trace = EventLog() + log_without_trace = EventLog() + for t in subset_with_trace: + log_with_trace.add_trace(t, log.log[tuple(t.nodes)]) + for t in subset_without_trace: + log_without_trace.add_trace(t, log.log[tuple(t.nodes)]) + + con_wt = self.determine_conformance_rate(log_with_trace) + con_wo = self.determine_conformance_rate(log_without_trace) + marginal_contribution = con_wt - con_wo + trace_contribution += weight * marginal_contribution shapley_values[str(trace.nodes)] = trace_contribution - + return shapley_values def evaluate_similarity(self, trace): @@ -491,40 +473,21 @@ def levenshtein_distance(seq1, seq2): #event_log = EventLog() exp = Explainer() exp.add_constraint('^A') -exp.add_constraint('C$') -trace0 = Trace(['A', 'B', 'C']) -trace1 = Trace(['B', 'C']) -trace2 = Trace(['A','B']) -trace3 = Trace(['B']) +exp.add_constraint('D$') +trace0 = Trace(['A', 'B', 'C', 'D']) +trace1 = Trace(['A', 'C', 'D', 'B']) +trace2 = Trace(['C']) +trace3 = Trace(['D']) eventlog = EventLog() -eventlog.add_trace(trace0, 5) +eventlog.add_trace(trace0, 10) eventlog.add_trace(trace1, 10) -eventlog.add_trace(trace2, 5) -eventlog.add_trace(trace3, 5) -print('Contribution ^a:', exp.determine_shapley_value(eventlog, exp.constraints, 0)) -print('Contribution c$:', exp.determine_shapley_value(eventlog, exp.constraints, 1)) -print(exp.determine_shapley_values_traces(eventlog)) -#exp.add_constraint('ABC') -#print(exp.conformant(Trace(['AXC']))) -#trace1 = Trace(['A','B','C']) -#trace2 = Trace(['B', 'C']) -#trace3 = Trace(['A', 'B']) -#trace4 = Trace(['B']) -# -## Adding traces -#event_log.add_trace(trace1, 5) -#event_log.add_trace(trace2, 10) -#event_log.add_trace(trace3, 5) -#event_log.add_trace(trace4, 5) -#for con in exp.constraints: -# print(f"existance constraints: {exp.identify_existance_constraints(con)}") -##print(event_log) -##print(exp.determine_conformance_rate(event_log)) -#print("False: " + str(exp.conformant(trace2))+ " activated: " + str(exp.activation(trace2))) -#print("False: " + str(exp.conformant(trace3))+ " activated: " + str(exp.activation(trace3))) -#print("False: " + str(exp.conformant(trace4))+ " activated: " + str(exp.activation(trace4))) -#print("True: " + str(exp.conformant(trace1))+ " activated: " + str(exp.activation(trace1))) -# -# -#print('Contribution ^a:', exp.determine_shapley_value(event_log, exp.constraints, 0)) -#print('Contribution c$:', exp.determine_shapley_value(event_log, exp.constraints, 1)) \ No newline at end of file +eventlog.add_trace(trace2, 10) +eventlog.add_trace(trace3, 20) +print('Conformance rate: ' + str(exp.determine_conformance_rate(eventlog))) +print('Contribution ^A:', exp.determine_shapley_value(eventlog, exp.constraints, 0)) +print('Contribution D$:', exp.determine_shapley_value(eventlog, exp.constraints, 1)) +values = exp.determine_shapley_values_traces(eventlog) +print(eventlog) +print("^A, D$") +for t, v in values.items(): + print(t +" : " + str(round(v, 2))) diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index ef25e03..5c979fa 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -27,8 +27,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "Contribution ^a: 0.5\n", - "Contribution c$: 0.30000000000000004\n" + "Conformance rate: 0.2\n", + "Contribution ^A: 0.5\n", + "Contribution D$: 0.30000000000000004\n", + "{('A', 'B', 'C', 'D'): 10, ('A', 'C', 'D', 'B'): 10, ('C',): 10, ('D',): 20}\n", + "^A, D$\n", + "['A', 'B', 'C', 'D'] : 0.2\n", + "['A', 'C', 'D', 'B'] : -0.15\n", + "['C'] : -0.15\n", + "['D'] : -0.19\n" ] } ], @@ -248,7 +255,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -291,6 +298,50 @@ "print('Contribution B+:', round(exp.determine_shapley_value(event_log, exp.constraints, 2), 2))\n", "\n" ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conformance rate: 0.2\n", + "Contribution ^A: 0.5\n", + "Contribution D$: 0.30000000000000004\n", + "{('A', 'B', 'C', 'D'): 10, ('A', 'C', 'D', 'B'): 10, ('C',): 10, ('D',): 20}\n", + "^A, D$\n", + "['A', 'B', 'C', 'D'] : 0.2\n", + "['A', 'C', 'D', 'B'] : -0.15\n", + "['C'] : -0.15\n", + "['D'] : -0.19\n" + ] + } + ], + "source": [ + "exp = Explainer()\n", + "exp.add_constraint('^A')\n", + "exp.add_constraint('D$')\n", + "trace0 = Trace(['A', 'B', 'C', 'D'])\n", + "trace1 = Trace(['A', 'C', 'D', 'B'])\n", + "trace2 = Trace(['C'])\n", + "trace3 = Trace(['D'])\n", + "eventlog = EventLog()\n", + "eventlog.add_trace(trace0, 10)\n", + "eventlog.add_trace(trace1, 10)\n", + "eventlog.add_trace(trace2, 10)\n", + "eventlog.add_trace(trace3, 20)\n", + "print('Conformance rate: ' + str(exp.determine_conformance_rate(eventlog)))\n", + "print('Contribution ^A:', exp.determine_shapley_value(eventlog, exp.constraints, 0))\n", + "print('Contribution D$:', exp.determine_shapley_value(eventlog, exp.constraints, 1))\n", + "values = exp.determine_shapley_values_traces(eventlog)\n", + "print(eventlog)\n", + "print(\"^A, D$\")\n", + "for t, v in values.items():\n", + " print(t +\" : \" + str(round(v, 2)))\n" + ] } ], "metadata": { From 78ac617d2113d9dc00def5c4bf0ab9d9efaacd4c Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 22 Mar 2024 13:03:57 +0100 Subject: [PATCH 13/54] Added comments --- explainer/explainer.py | 159 +++++++++++--------- explainer/tutorial/explainer_tutorial.ipynb | 62 +------- 2 files changed, 93 insertions(+), 128 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 1938f6d..ba40cf9 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -1,7 +1,6 @@ -import itertools import math import re -from itertools import combinations, permutations, product,combinations_with_replacement, chain +from itertools import combinations, product, chain class Trace: def __init__(self, nodes): @@ -12,12 +11,21 @@ def __init__(self, nodes): """ self.nodes = nodes def __len__(self): + """ + Returns the number of nodes in the trace. + """ return len(self.nodes) def __iter__(self): + """ + Initializes the iteration over the nodes in the trace. + """ self.index = 0 return self def __next__(self): + """ + Returns the next node in the trace during iteration. + """ if self.index < len(self.nodes): result = self.nodes[self.index] self.index += 1 @@ -25,6 +33,11 @@ def __next__(self): else: raise StopIteration def __split__(self): + """ + Splits the nodes of the trace into a list. + + :return: A list containing the nodes of the trace. + """ spl = [] for node in self.nodes: spl.append(node) @@ -64,12 +77,13 @@ def remove_trace(self, trace, count = 1): self.log[trace_tuple] -= count else: del self.log[trace_tuple] - def get_trace_cardinality(self, trace): - trace_tuple = tuple(trace.nodes) - if trace_tuple in self.log: - return self.log[trace_tuple] + def __str__(self): + """ + Returns a string representation of the event log. + """ return str(self.log) + def __len__(self): """ Returns the total number of trace occurrences in the log. @@ -149,7 +163,12 @@ def activation(self, trace, constraints = None): return con_activation def identify_existance_constraints(self, pattern): + """ + Identifies existance constraints within a pattern. + :param pattern: The constraint pattern as a string. + :return: A tuple indicating the type of existance constraint and the node involved. + """ # Check for AtLeastOne constraint for match in re.finditer(r'(? 100: @@ -255,6 +293,15 @@ def counter_factual_helper(self, working_trace, explanation, depth = 0): def operate_on_trace(self, trace, score, explanation_path, depth = 0): + """ + Finds and applies modifications to the trace to make it conformant. + + :param trace: The trace to be modified. + :param score: The similarity score of the trace. + :param explanation_path: The current explanation path. + :param depth: The current recursion depth. + :return: A string explaining why the best subtrace is non-conformant or a message indicating the maximum depth has been reached. + """ explanation = None counter_factuals = self.modify_subtrace(trace) best_subtrace = None @@ -346,46 +393,13 @@ def determine_shapley_value(self, log, constraints, index): sub_ctrbs.append(sub_ctrb) return sum(sub_ctrbs) - def determine_shapley_values_traces(self, log): + def evaluate_similarity(self, trace): """ - Determines the Shapley value-based contribution of each trace to the overall conformance rate. + Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance. - Args: - log (EventLog): The event log containing the traces. - - Returns: - dict: A dictionary with trace representations as keys and their Shapley values as values. + :param trace: The trace to compare with the adherent trace. + :return: A normalized score indicating the similarity between the adherent trace and the given trace. """ - shapley_values = {} - traces = [Trace(list(t)) for t in log.log.keys()] # Convert trace tuples to Trace instances - total_traces = len(traces) - - for trace in traces: - trace_contribution = 0 - for subset_size in range(total_traces + 1): - for subset in combinations(traces, subset_size): - weight = 1 / (total_traces * (total_traces - 1)) # Recalculate weight for each subset size - subset_with_trace = list(subset) + [trace] - subset_without_trace = list(subset) - if trace not in subset: # Trace not in this subset - subset_with_trace = list(subset) + [trace] - subset_without_trace = list(subset) - log_with_trace = EventLog() - log_without_trace = EventLog() - for t in subset_with_trace: - log_with_trace.add_trace(t, log.log[tuple(t.nodes)]) - for t in subset_without_trace: - log_without_trace.add_trace(t, log.log[tuple(t.nodes)]) - - con_wt = self.determine_conformance_rate(log_with_trace) - con_wo = self.determine_conformance_rate(log_without_trace) - marginal_contribution = con_wt - con_wo - trace_contribution += weight * marginal_contribution - shapley_values[str(trace.nodes)] = trace_contribution - - return shapley_values - - def evaluate_similarity(self, trace): length = len(self.adherent_trace) trace_len = len("".join(trace)) lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) @@ -394,6 +408,13 @@ def evaluate_similarity(self, trace): return normalized_score def determine_conformance_rate(self, event_log, constraints = None): + """ + Determines the conformance rate of the event log based on the given constraints. + + :param event_log: The event log to analyze. + :param constraints: The constraints to check against the event log. + :return: The conformance rate as a float between 0 and 1, or a message if no constraints are provided. + """ if not self.constraints and not constraints: return "The explainer have no constraints" len_log = len(event_log) @@ -409,6 +430,25 @@ def determine_conformance_rate(self, event_log, constraints = None): break return (len_log - non_conformant) / len_log + def trace_contribution_to_conformance_loss(self, event_log, trace, constraints = None): + """ + Calculates the contribution of a specific trace to the conformance loss of the event log. + + :param event_log: The event log to analyze. + :param trace: The trace to calculate its contribution. + :param constraints: The constraints to check against the event log. + :return: The contribution of the trace to the conformance loss as a float between 0 and 1. + """ + if not constraints: + constraints = self.constraints + total_traces = len(event_log) + contribution_of_trace = 0 + for t, count in event_log.log.items(): + if not self.conformant(t, constraints): + if trace.nodes == list(t): + contribution_of_trace = count + + return contribution_of_trace / total_traces def determine_powerset(elements): """Determines the powerset of a list of elements @@ -449,6 +489,13 @@ def get_iterative_subtrace(trace): def levenshtein_distance(seq1, seq2): """ Calculates the Levenshtein distance between two sequences. + + Args: + seq1 (str): The first sequence. + seq2 (str): The second sequence. + + Returns: + int: The Levenshtein distance between the two sequences. """ size_x = len(seq1) + 1 size_y = len(seq2) + 1 @@ -468,26 +515,4 @@ def levenshtein_distance(seq1, seq2): matrix[x][y-1] + 1, matrix[x-1][y-1] + 1 ) - return matrix[size_x-1][size_y-1] - -#event_log = EventLog() -exp = Explainer() -exp.add_constraint('^A') -exp.add_constraint('D$') -trace0 = Trace(['A', 'B', 'C', 'D']) -trace1 = Trace(['A', 'C', 'D', 'B']) -trace2 = Trace(['C']) -trace3 = Trace(['D']) -eventlog = EventLog() -eventlog.add_trace(trace0, 10) -eventlog.add_trace(trace1, 10) -eventlog.add_trace(trace2, 10) -eventlog.add_trace(trace3, 20) -print('Conformance rate: ' + str(exp.determine_conformance_rate(eventlog))) -print('Contribution ^A:', exp.determine_shapley_value(eventlog, exp.constraints, 0)) -print('Contribution D$:', exp.determine_shapley_value(eventlog, exp.constraints, 1)) -values = exp.determine_shapley_values_traces(eventlog) -print(eventlog) -print("^A, D$") -for t, v in values.items(): - print(t +" : " + str(round(v, 2))) + return matrix[size_x-1][size_y-1] \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index 5c979fa..c8c0504 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -22,23 +22,7 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Conformance rate: 0.2\n", - "Contribution ^A: 0.5\n", - "Contribution D$: 0.30000000000000004\n", - "{('A', 'B', 'C', 'D'): 10, ('A', 'C', 'D', 'B'): 10, ('C',): 10, ('D',): 20}\n", - "^A, D$\n", - "['A', 'B', 'C', 'D'] : 0.2\n", - "['A', 'C', 'D', 'B'] : -0.15\n", - "['C'] : -0.15\n", - "['D'] : -0.19\n" - ] - } - ], + "outputs": [], "source": [ "import sys\n", "sys.path.append('../')\n", @@ -298,50 +282,6 @@ "print('Contribution B+:', round(exp.determine_shapley_value(event_log, exp.constraints, 2), 2))\n", "\n" ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Conformance rate: 0.2\n", - "Contribution ^A: 0.5\n", - "Contribution D$: 0.30000000000000004\n", - "{('A', 'B', 'C', 'D'): 10, ('A', 'C', 'D', 'B'): 10, ('C',): 10, ('D',): 20}\n", - "^A, D$\n", - "['A', 'B', 'C', 'D'] : 0.2\n", - "['A', 'C', 'D', 'B'] : -0.15\n", - "['C'] : -0.15\n", - "['D'] : -0.19\n" - ] - } - ], - "source": [ - "exp = Explainer()\n", - "exp.add_constraint('^A')\n", - "exp.add_constraint('D$')\n", - "trace0 = Trace(['A', 'B', 'C', 'D'])\n", - "trace1 = Trace(['A', 'C', 'D', 'B'])\n", - "trace2 = Trace(['C'])\n", - "trace3 = Trace(['D'])\n", - "eventlog = EventLog()\n", - "eventlog.add_trace(trace0, 10)\n", - "eventlog.add_trace(trace1, 10)\n", - "eventlog.add_trace(trace2, 10)\n", - "eventlog.add_trace(trace3, 20)\n", - "print('Conformance rate: ' + str(exp.determine_conformance_rate(eventlog)))\n", - "print('Contribution ^A:', exp.determine_shapley_value(eventlog, exp.constraints, 0))\n", - "print('Contribution D$:', exp.determine_shapley_value(eventlog, exp.constraints, 1))\n", - "values = exp.determine_shapley_values_traces(eventlog)\n", - "print(eventlog)\n", - "print(\"^A, D$\")\n", - "for t, v in values.items():\n", - " print(t +\" : \" + str(round(v, 2)))\n" - ] } ], "metadata": { From 55ac8e7093b219f7d233046a4099238c78fe7c94 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 22 Mar 2024 13:10:03 +0100 Subject: [PATCH 14/54] Linting issues --- bpmnconstraints/script.py | 1 + bpmnconstraints/utils/plot.py | 1 + tests/explainer/explainer_test.py | 26 +++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/bpmnconstraints/script.py b/bpmnconstraints/script.py index 6f7eb1e..060ca00 100644 --- a/bpmnconstraints/script.py +++ b/bpmnconstraints/script.py @@ -1,4 +1,5 @@ """Entry point for bpmnsignal command. Verifies argument and runs parser.""" + # pylint: disable=import-error import argparse import logging diff --git a/bpmnconstraints/utils/plot.py b/bpmnconstraints/utils/plot.py index 7a5990c..434b782 100644 --- a/bpmnconstraints/utils/plot.py +++ b/bpmnconstraints/utils/plot.py @@ -1,6 +1,7 @@ """ Module for plotting functions. """ + # pylint: disable=import-error import matplotlib.pyplot as plt diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 823b32b..72689eb 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -1,18 +1,22 @@ from explainer.explainer import * + # Test 1: Adding and checking constraints def test_add_constraint(): explainer = Explainer() explainer.add_constraint('A.*B.*C') assert 'A.*B.*C' in explainer.constraints, "Constraint 'A.*B.*C' should be added." + # Test 2: Removing constraints def test_remove_constraint(): explainer = Explainer() explainer.add_constraint('A.*B.*C') explainer.add_constraint('B.*C') explainer.remove_constraint(0) - assert 'A.*B.*C' not in explainer.constraints, "Constraint 'A.*B.*C' should be removed." + assert ('A.*B.*C' not in explainer.constraints, + ),"Constraint 'A.*B.*C' should be removed." + # Test 3: Activation of constraints def test_activation(): @@ -28,6 +32,7 @@ def test_conformance(): explainer.add_constraint('A.*B.*C') assert explainer.conformant(trace), "The trace should be conformant." + # Test 5: Non-conformance explanation def test_non_conformance_explanation(): trace = Trace(['C', 'A', 'B']) @@ -36,6 +41,7 @@ def test_non_conformance_explanation(): explanation = explainer.minimal_expl(trace) assert "violated" in explanation, "The explanation should indicate a violation." + # Test 6: Overlapping constraints def test_overlapping_constraints(): trace = Trace(['A', 'B', 'A', 'C']) @@ -44,6 +50,7 @@ def test_overlapping_constraints(): explainer.add_constraint('A.*A.*C') assert explainer.conformant(trace), "The trace should be conformant with overlapping constraints." + # Test 7: Partially meeting constraints def test_partial_conformance(): trace = Trace(['A', 'C', 'B']) @@ -51,6 +58,7 @@ def test_partial_conformance(): explainer.add_constraint('A.*B.*C') assert not explainer.conformant(trace), "The trace should not be fully conformant." + # Test 8: Constraints with repeated nodes def test_constraints_with_repeated_nodes(): trace = Trace(['A', 'A', 'B', 'A']) @@ -58,6 +66,7 @@ def test_constraints_with_repeated_nodes(): explainer.add_constraint('A.*A.*B.*A') assert explainer.conformant(trace), "The trace should conform to the constraint with repeated nodes." + # Test 9: Removing constraints and checking nodes list def test_remove_constraint_and_check_nodes(): explainer = Explainer() @@ -66,6 +75,7 @@ def test_remove_constraint_and_check_nodes(): explainer.remove_constraint(0) assert 'A' not in explainer.nodes and 'B' in explainer.nodes and 'C' in explainer.nodes, "Node 'A' should be removed, while 'B' and 'C' remain." + # Test 10: Complex regex constraint def test_complex_regex_constraint(): trace = Trace(['A', 'X', 'B', 'Y', 'C']) @@ -73,6 +83,7 @@ def test_complex_regex_constraint(): explainer.add_constraint('A.*X.*B.*Y.*C') # Specifically expects certain nodes in order assert explainer.conformant(trace), "The trace should conform to the complex regex constraint." + # Test 11: Constraint not covered by any trace node def test_constraint_not_covered(): trace = Trace(['A', 'B', 'C']) @@ -80,6 +91,7 @@ def test_constraint_not_covered(): explainer.add_constraint('D*') # This node 'D' does not exist in the trace assert explainer.activation(trace) == [0], "The constraint should not be activated by the trace." + # Test 12: Empty trace and constraints def test_empty_trace_and_constraints(): trace = Trace([]) @@ -87,6 +99,7 @@ def test_empty_trace_and_constraints(): explainer.add_constraint('') # Adding an empty constraint assert explainer.conformant(trace), "An empty trace should be conformant with an empty constraint." + # Test 13: Removing non-existent constraint index def test_remove_nonexistent_constraint(): explainer = Explainer() @@ -94,12 +107,14 @@ def test_remove_nonexistent_constraint(): explainer.remove_constraint(10) # Non-existent index assert len(explainer.constraints) == 1, "Removing a non-existent constraint should not change the constraints list." + # Test 14: Activation with no constraints def test_activation_with_no_constraints(): trace = Trace(['A', 'B', 'C']) explainer = Explainer() assert not explainer.activation(trace), "No constraints should mean no activation." + # Test 15: Trace conformance against multiple constraints def test_trace_conformance_against_multiple_constraints(): trace1 = Trace(['A', 'B', 'D']) # This trace should not be fully conformant as it only matches one constraint @@ -113,6 +128,7 @@ def test_trace_conformance_against_multiple_constraints(): assert not explainer.conformant(trace1), "Trace1 should not be conformant as it does not satisfy all constraints." assert explainer.conformant(trace2), "Trace2 should be conformant as it satisfies all constraints." + # Test 16: Conformant trace does not generate minimal explaination def test_conformant_trace_handled_correctly(): trace = Trace(['A', 'B']) @@ -120,6 +136,8 @@ def test_conformant_trace_handled_correctly(): explainer.add_constraint('AB') print(explainer.minimal_expl(trace)) assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed." + + # Test 17: Conformant trace def test_explainer_methods(): trace = Trace(['A', 'B', 'C']) @@ -131,6 +149,8 @@ def test_explainer_methods(): assert explainer.conformant(trace) == True, "Test 1 Failed: Trace should be conformant." assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed.", "Test 1 Failed: Incorrect minimal explanation for a conformant trace." assert explainer.counterfactual_expl(trace) == "The trace is already conformant, no changes needed.", "Test 1 Failed: Incorrect counterfactual explanation for a conformant trace." + + # Test 18: Some explaination test def test_explaination(): explainer = Explainer() @@ -144,6 +164,8 @@ def test_explaination(): assert explainer.conformant(conformant_trace) == True assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')" assert explainer.counterfactual_expl(non_conformant_trace) == "\nAddition (Added B at position 1): A->B->C" + + # Test 19: Complex explaination test. """ This part is not very complex as of now and is very much up for change, the complexity of counterfactuals @@ -160,6 +182,7 @@ def test_complex_counterfactual_explanation(): assert counterfactual_explanation == "\nAddition (Added B at position 1): A->B->C->E->D" + # Test 20: Event logs def test_event_log(): event_log = EventLog() @@ -180,3 +203,4 @@ def test_event_log(): event_log.add_trace(trace, 5) event_log.add_trace(trace2, 7) assert event_log.log == {('A', 'B', 'C'): 5,('X', 'Y', 'Z'): 7} # There should be several traces in the log + From 05b67ce5f4ff6f143a11b8d724a154b5d70f0397 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 22 Mar 2024 13:13:35 +0100 Subject: [PATCH 15/54] Linting --- setup.py | 1 + tests/explainer/explainer_test.py | 98 +++++++++++++++---------------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/setup.py b/setup.py index dffd6fe..2882653 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ """Setup for running the bpmnconstraints script.""" + import setuptools with open("README.md", encoding="utf-8") as file: diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 72689eb..139caf8 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -4,91 +4,91 @@ # Test 1: Adding and checking constraints def test_add_constraint(): explainer = Explainer() - explainer.add_constraint('A.*B.*C') - assert 'A.*B.*C' in explainer.constraints, "Constraint 'A.*B.*C' should be added." + explainer.add_constraint("A.*B.*C") + assert "A.*B.*C" in explainer.constraints, "Constraint 'A.*B.*C' should be added." # Test 2: Removing constraints def test_remove_constraint(): explainer = Explainer() - explainer.add_constraint('A.*B.*C') - explainer.add_constraint('B.*C') + explainer.add_constraint("A.*B.*C") + explainer.add_constraint("B.*C") explainer.remove_constraint(0) - assert ('A.*B.*C' not in explainer.constraints, + assert ("A.*B.*C" not in explainer.constraints, ),"Constraint 'A.*B.*C' should be removed." # Test 3: Activation of constraints def test_activation(): - trace = Trace(['A', 'B', 'C']) + trace = Trace(["A", "B", "C"]) explainer = Explainer() - explainer.add_constraint('A.*B.*C') + explainer.add_constraint("A.*B.*C") assert explainer.activation(trace), "The trace should activate the constraint." # Test 4: Checking conformance of traces def test_conformance(): - trace = Trace(['A', 'B', 'C']) + trace = Trace(["A", "B", "C"]) explainer = Explainer() - explainer.add_constraint('A.*B.*C') + explainer.add_constraint("A.*B.*C") assert explainer.conformant(trace), "The trace should be conformant." # Test 5: Non-conformance explanation def test_non_conformance_explanation(): - trace = Trace(['C', 'A', 'B']) + trace = Trace(["C", "A", "B"]) explainer = Explainer() - explainer.add_constraint('A.*B.*C') + explainer.add_constraint("A.*B.*C") explanation = explainer.minimal_expl(trace) assert "violated" in explanation, "The explanation should indicate a violation." # Test 6: Overlapping constraints def test_overlapping_constraints(): - trace = Trace(['A', 'B', 'A', 'C']) + trace = Trace(["A", "B", "A", "C"]) explainer = Explainer() - explainer.add_constraint('A.*B.*C') - explainer.add_constraint('A.*A.*C') + explainer.add_constraint("A.*B.*C") + explainer.add_constraint("A.*A.*C") assert explainer.conformant(trace), "The trace should be conformant with overlapping constraints." # Test 7: Partially meeting constraints def test_partial_conformance(): - trace = Trace(['A', 'C', 'B']) + trace = Trace(["A", "C", "B"]) explainer = Explainer() - explainer.add_constraint('A.*B.*C') + explainer.add_constraint("A.*B.*C") assert not explainer.conformant(trace), "The trace should not be fully conformant." # Test 8: Constraints with repeated nodes def test_constraints_with_repeated_nodes(): - trace = Trace(['A', 'A', 'B', 'A']) + trace = Trace(["A", "A", "B", "A"]) explainer = Explainer() - explainer.add_constraint('A.*A.*B.*A') + explainer.add_constraint("A.*A.*B.*A") assert explainer.conformant(trace), "The trace should conform to the constraint with repeated nodes." # Test 9: Removing constraints and checking nodes list def test_remove_constraint_and_check_nodes(): explainer = Explainer() - explainer.add_constraint('A.*B') - explainer.add_constraint('B.*C') + explainer.add_constraint("A.*B") + explainer.add_constraint("B.*C") explainer.remove_constraint(0) - assert 'A' not in explainer.nodes and 'B' in explainer.nodes and 'C' in explainer.nodes, "Node 'A' should be removed, while 'B' and 'C' remain." + assert "A" not in explainer.nodes and "B" in explainer.nodes and "C" in explainer.nodes, "Node 'A' should be removed, while 'B' and 'C' remain." # Test 10: Complex regex constraint def test_complex_regex_constraint(): - trace = Trace(['A', 'X', 'B', 'Y', 'C']) + trace = Trace(["A", "X", "B", "Y", "C"]) explainer = Explainer() - explainer.add_constraint('A.*X.*B.*Y.*C') # Specifically expects certain nodes in order + explainer.add_constraint("A.*X.*B.*Y.*C") # Specifically expects certain nodes in order assert explainer.conformant(trace), "The trace should conform to the complex regex constraint." # Test 11: Constraint not covered by any trace node def test_constraint_not_covered(): - trace = Trace(['A', 'B', 'C']) + trace = Trace(["A", "B", "C"]) explainer = Explainer() - explainer.add_constraint('D*') # This node 'D' does not exist in the trace + explainer.add_constraint("D*") # This node "D" does not exist in the trace assert explainer.activation(trace) == [0], "The constraint should not be activated by the trace." @@ -96,33 +96,33 @@ def test_constraint_not_covered(): def test_empty_trace_and_constraints(): trace = Trace([]) explainer = Explainer() - explainer.add_constraint('') # Adding an empty constraint + explainer.add_constraint("") # Adding an empty constraint assert explainer.conformant(trace), "An empty trace should be conformant with an empty constraint." # Test 13: Removing non-existent constraint index def test_remove_nonexistent_constraint(): explainer = Explainer() - explainer.add_constraint('A.*B') + explainer.add_constraint("A.*B") explainer.remove_constraint(10) # Non-existent index assert len(explainer.constraints) == 1, "Removing a non-existent constraint should not change the constraints list." # Test 14: Activation with no constraints def test_activation_with_no_constraints(): - trace = Trace(['A', 'B', 'C']) + trace = Trace(["A", "B", "C"]) explainer = Explainer() assert not explainer.activation(trace), "No constraints should mean no activation." # Test 15: Trace conformance against multiple constraints def test_trace_conformance_against_multiple_constraints(): - trace1 = Trace(['A', 'B', 'D']) # This trace should not be fully conformant as it only matches one constraint - trace2 = Trace(['A', 'B', 'C', 'D']) # This trace should be conformant as it matches both constraints + trace1 = Trace(["A", "B", "D"]) # This trace should not be fully conformant as it only matches one constraint + trace2 = Trace(["A", "B", "C", "D"]) # This trace should be conformant as it matches both constraints explainer = Explainer() - explainer.add_constraint('A.*B.*C') # Both traces attempt to conform to this - explainer.add_constraint('B.*D') # And to this + explainer.add_constraint("A.*B.*C") # Both traces attempt to conform to this + explainer.add_constraint("B.*D") # And to this # Checking conformance assert not explainer.conformant(trace1), "Trace1 should not be conformant as it does not satisfy all constraints." @@ -131,19 +131,19 @@ def test_trace_conformance_against_multiple_constraints(): # Test 16: Conformant trace does not generate minimal explaination def test_conformant_trace_handled_correctly(): - trace = Trace(['A', 'B']) + trace = Trace(["A", "B"]) explainer = Explainer() - explainer.add_constraint('AB') + explainer.add_constraint("AB") print(explainer.minimal_expl(trace)) assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed." # Test 17: Conformant trace def test_explainer_methods(): - trace = Trace(['A', 'B', 'C']) + trace = Trace(["A", "B", "C"]) explainer = Explainer() - explainer.add_constraint('A.*B.*C') - explainer.add_constraint('B.*C') + explainer.add_constraint("A.*B.*C") + explainer.add_constraint("B.*C") assert explainer.conformant(trace) == True, "Test 1 Failed: Trace should be conformant." @@ -155,14 +155,14 @@ def test_explainer_methods(): def test_explaination(): explainer = Explainer() - conformant_trace = Trace(['A','B','C']) - non_conformant_trace = Trace(['A','C']) + conformant_trace = Trace(["A","B","C"]) + non_conformant_trace = Trace(["A","C"]) - explainer.add_constraint('A.*B.*C') + explainer.add_constraint("A.*B.*C") assert explainer.conformant(non_conformant_trace) == False assert explainer.conformant(conformant_trace) == True - assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')" + assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ("A", "C")" assert explainer.counterfactual_expl(non_conformant_trace) == "\nAddition (Added B at position 1): A->B->C" @@ -174,9 +174,9 @@ def test_explaination(): def test_complex_counterfactual_explanation(): explainer = Explainer() - explainer.add_constraint('ABB*C') + explainer.add_constraint("ABB*C") - non_conformant_trace = Trace(['A', 'C', 'E', 'D']) + non_conformant_trace = Trace(["A", "C", "E", "D"]) counterfactual_explanation = explainer.counterfactual_expl(non_conformant_trace) @@ -187,20 +187,20 @@ def test_complex_counterfactual_explanation(): def test_event_log(): event_log = EventLog() assert event_log != None - trace = Trace(['A', 'B', 'C']) + trace = Trace(["A", "B", "C"]) event_log.add_trace(trace) - assert event_log.log == {('A', 'B', 'C'): 1} # There should be one instance of the trace in the log + assert event_log.log == {("A", "B", "C"): 1} # There should be one instance of the trace in the log event_log.add_trace(trace, 5) - assert event_log.log == {('A', 'B', 'C'): 6} # There should be 6 instances of the trace in the log + assert event_log.log == {("A", "B", "C"): 6} # There should be 6 instances of the trace in the log event_log.remove_trace(trace) - assert event_log.log == {('A', 'B', 'C'): 5} # There should be 5 instances of the trace + assert event_log.log == {("A", "B", "C"): 5} # There should be 5 instances of the trace event_log.remove_trace(trace, 5) assert event_log.log == {} # The log should be emptied event_log.add_trace(trace, 5) event_log.remove_trace(trace, 10) assert event_log.log == {} # The log should be emptied - trace2 = Trace(['X', 'Y', 'Z']) + trace2 = Trace(["X", "Y", "Z"]) event_log.add_trace(trace, 5) event_log.add_trace(trace2, 7) - assert event_log.log == {('A', 'B', 'C'): 5,('X', 'Y', 'Z'): 7} # There should be several traces in the log + assert event_log.log == {("A", "B", "C"): 5,("X", "Y", "Z"): 7} # There should be several traces in the log From 7818b3ae9ca77d2df3490e020355949111672a08 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 22 Mar 2024 13:14:02 +0100 Subject: [PATCH 16/54] Linting --- tests/explainer/explainer_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 139caf8..19c52c9 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -162,7 +162,7 @@ def test_explaination(): assert explainer.conformant(non_conformant_trace) == False assert explainer.conformant(conformant_trace) == True - assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ("A", "C")" + assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')" assert explainer.counterfactual_expl(non_conformant_trace) == "\nAddition (Added B at position 1): A->B->C" From 384e06d101d7d65db528a0b5929b3caf92494a48 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 22 Mar 2024 13:22:27 +0100 Subject: [PATCH 17/54] more linting --- tests/explainer/explainer_test.py | 119 ++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 19c52c9..2352f75 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -14,8 +14,9 @@ def test_remove_constraint(): explainer.add_constraint("A.*B.*C") explainer.add_constraint("B.*C") explainer.remove_constraint(0) - assert ("A.*B.*C" not in explainer.constraints, - ),"Constraint 'A.*B.*C' should be removed." + assert ( + "A.*B.*C" not in explainer.constraints, + ),"Constraint 'A.*B.*C' should be removed." # Test 3: Activation of constraints @@ -25,6 +26,7 @@ def test_activation(): explainer.add_constraint("A.*B.*C") assert explainer.activation(trace), "The trace should activate the constraint." + # Test 4: Checking conformance of traces def test_conformance(): trace = Trace(["A", "B", "C"]) @@ -48,8 +50,9 @@ def test_overlapping_constraints(): explainer = Explainer() explainer.add_constraint("A.*B.*C") explainer.add_constraint("A.*A.*C") - assert explainer.conformant(trace), "The trace should be conformant with overlapping constraints." - + assert explainer.conformant( + trace + ), "The trace should be conformant with overlapping constraints." # Test 7: Partially meeting constraints def test_partial_conformance(): @@ -64,8 +67,9 @@ def test_constraints_with_repeated_nodes(): trace = Trace(["A", "A", "B", "A"]) explainer = Explainer() explainer.add_constraint("A.*A.*B.*A") - assert explainer.conformant(trace), "The trace should conform to the constraint with repeated nodes." - + assert explainer.conformant( + trace + ), "The trace should conform to the constraint with repeated nodes." # Test 9: Removing constraints and checking nodes list def test_remove_constraint_and_check_nodes(): @@ -73,15 +77,20 @@ def test_remove_constraint_and_check_nodes(): explainer.add_constraint("A.*B") explainer.add_constraint("B.*C") explainer.remove_constraint(0) - assert "A" not in explainer.nodes and "B" in explainer.nodes and "C" in explainer.nodes, "Node 'A' should be removed, while 'B' and 'C' remain." - + assert ( + "A" not in explainer.nodes and "B" in explainer.nodes and "C" in explainer.nodes + ), "Node 'A' should be removed, while 'B' and 'C' remain." # Test 10: Complex regex constraint def test_complex_regex_constraint(): trace = Trace(["A", "X", "B", "Y", "C"]) explainer = Explainer() - explainer.add_constraint("A.*X.*B.*Y.*C") # Specifically expects certain nodes in order - assert explainer.conformant(trace), "The trace should conform to the complex regex constraint." + explainer.add_constraint( + "A.*X.*B.*Y.*C" + ) # Specifically expects certain nodes in order + assert explainer.conformant( + trace + ), "The trace should conform to the complex regex constraint." # Test 11: Constraint not covered by any trace node @@ -89,15 +98,18 @@ def test_constraint_not_covered(): trace = Trace(["A", "B", "C"]) explainer = Explainer() explainer.add_constraint("D*") # This node "D" does not exist in the trace - assert explainer.activation(trace) == [0], "The constraint should not be activated by the trace." - + assert explainer.activation(trace) == [ + 0 + ], "The constraint should not be activated by the trace." # Test 12: Empty trace and constraints def test_empty_trace_and_constraints(): trace = Trace([]) explainer = Explainer() explainer.add_constraint("") # Adding an empty constraint - assert explainer.conformant(trace), "An empty trace should be conformant with an empty constraint." + assert explainer.conformant( + trace + ), "An empty trace should be conformant with an empty constraint." # Test 13: Removing non-existent constraint index @@ -105,8 +117,9 @@ def test_remove_nonexistent_constraint(): explainer = Explainer() explainer.add_constraint("A.*B") explainer.remove_constraint(10) # Non-existent index - assert len(explainer.constraints) == 1, "Removing a non-existent constraint should not change the constraints list." - + assert ( + len(explainer.constraints) == 1 + ), "Removing a non-existent constraint should not change the constraints list." # Test 14: Activation with no constraints def test_activation_with_no_constraints(): @@ -117,16 +130,24 @@ def test_activation_with_no_constraints(): # Test 15: Trace conformance against multiple constraints def test_trace_conformance_against_multiple_constraints(): - trace1 = Trace(["A", "B", "D"]) # This trace should not be fully conformant as it only matches one constraint - trace2 = Trace(["A", "B", "C", "D"]) # This trace should be conformant as it matches both constraints + trace1 = Trace( + ["A", "B", "D"] + ) # This trace should not be fully conformant as it only matches one constraint + trace2 = Trace( + ["A", "B", "C", "D"] + ) # This trace should be conformant as it matches both constraints explainer = Explainer() explainer.add_constraint("A.*B.*C") # Both traces attempt to conform to this explainer.add_constraint("B.*D") # And to this # Checking conformance - assert not explainer.conformant(trace1), "Trace1 should not be conformant as it does not satisfy all constraints." - assert explainer.conformant(trace2), "Trace2 should be conformant as it satisfies all constraints." + assert not explainer.conformant( + trace1 + ), "Trace1 should not be conformant as it does not satisfy all constraints." + assert explainer.conformant( + trace2 + ), "Trace2 should be conformant as it satisfies all constraints." # Test 16: Conformant trace does not generate minimal explaination @@ -134,7 +155,7 @@ def test_conformant_trace_handled_correctly(): trace = Trace(["A", "B"]) explainer = Explainer() explainer.add_constraint("AB") - print(explainer.minimal_expl(trace)) + assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed." @@ -142,13 +163,20 @@ def test_conformant_trace_handled_correctly(): def test_explainer_methods(): trace = Trace(["A", "B", "C"]) explainer = Explainer() - explainer.add_constraint("A.*B.*C") - explainer.add_constraint("B.*C") + explainer.add_constraint("A.*B.*C") + explainer.add_constraint("B.*C") - - assert explainer.conformant(trace) == True, "Test 1 Failed: Trace should be conformant." - assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed.", "Test 1 Failed: Incorrect minimal explanation for a conformant trace." - assert explainer.counterfactual_expl(trace) == "The trace is already conformant, no changes needed.", "Test 1 Failed: Incorrect counterfactual explanation for a conformant trace." + assert ( + explainer.conformant(trace) == True + ), "Test 1 Failed: Trace should be conformant." + assert ( + explainer.minimal_expl(trace) + == "The trace is already conformant, no changes needed." + ), "Test 1 Failed: Incorrect minimal explanation for a conformant trace." + assert ( + explainer.counterfactual_expl(trace) + == "The trace is already conformant, no changes needed." + ), "Test 1 Failed: Incorrect counterfactual explanation for a conformant trace." # Test 18: Some explaination test @@ -162,8 +190,14 @@ def test_explaination(): assert explainer.conformant(non_conformant_trace) == False assert explainer.conformant(conformant_trace) == True - assert explainer.minimal_expl(non_conformant_trace) == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')" - assert explainer.counterfactual_expl(non_conformant_trace) == "\nAddition (Added B at position 1): A->B->C" + assert ( + explainer.minimal_expl(non_conformant_trace) + == "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')" + ) + assert ( + explainer.counterfactual_expl(non_conformant_trace) + == "\nAddition (Added B at position 1): A->B->C" + ) # Test 19: Complex explaination test. @@ -180,8 +214,10 @@ def test_complex_counterfactual_explanation(): counterfactual_explanation = explainer.counterfactual_expl(non_conformant_trace) - assert counterfactual_explanation == "\nAddition (Added B at position 1): A->B->C->E->D" - + assert ( + counterfactual_explanation + == "\nAddition (Added B at position 1): A->B->C->E->D" + ) # Test 20: Event logs def test_event_log(): @@ -189,18 +225,27 @@ def test_event_log(): assert event_log != None trace = Trace(["A", "B", "C"]) event_log.add_trace(trace) - assert event_log.log == {("A", "B", "C"): 1} # There should be one instance of the trace in the log + assert event_log.log == { + ("A", "B", "C"): 1 + } # There should be one instance of the trace in the log event_log.add_trace(trace, 5) - assert event_log.log == {("A", "B", "C"): 6} # There should be 6 instances of the trace in the log + assert event_log.log == { + ("A", "B", "C"): 6 + } # There should be 6 instances of the trace in the log event_log.remove_trace(trace) - assert event_log.log == {("A", "B", "C"): 5} # There should be 5 instances of the trace + assert event_log.log == { + ("A", "B", "C"): 5 + } # There should be 5 instances of the trace event_log.remove_trace(trace, 5) - assert event_log.log == {} # The log should be emptied - event_log.add_trace(trace, 5) + assert event_log.log == {} # The log should be emptied + event_log.add_trace(trace, 5) event_log.remove_trace(trace, 10) - assert event_log.log == {} # The log should be emptied + assert event_log.log == {} # The log should be emptied trace2 = Trace(["X", "Y", "Z"]) event_log.add_trace(trace, 5) event_log.add_trace(trace2, 7) - assert event_log.log == {("A", "B", "C"): 5,("X", "Y", "Z"): 7} # There should be several traces in the log + assert event_log.log == { + ("A", "B", "C"): 5, + ("X", "Y", "Z"): 7, + } # There should be several traces in the log From baf65df510d59f7c1d7fa92c898e1e2ff48298d6 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 22 Mar 2024 13:27:02 +0100 Subject: [PATCH 18/54] More linting --- tests/explainer/explainer_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 2352f75..0d95126 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -16,7 +16,7 @@ def test_remove_constraint(): explainer.remove_constraint(0) assert ( "A.*B.*C" not in explainer.constraints, - ),"Constraint 'A.*B.*C' should be removed." + ), "Constraint 'A.*B.*C' should be removed." # Test 3: Activation of constraints @@ -54,6 +54,7 @@ def test_overlapping_constraints(): trace ), "The trace should be conformant with overlapping constraints." + # Test 7: Partially meeting constraints def test_partial_conformance(): trace = Trace(["A", "C", "B"]) @@ -71,6 +72,7 @@ def test_constraints_with_repeated_nodes(): trace ), "The trace should conform to the constraint with repeated nodes." + # Test 9: Removing constraints and checking nodes list def test_remove_constraint_and_check_nodes(): explainer = Explainer() @@ -81,6 +83,7 @@ def test_remove_constraint_and_check_nodes(): "A" not in explainer.nodes and "B" in explainer.nodes and "C" in explainer.nodes ), "Node 'A' should be removed, while 'B' and 'C' remain." + # Test 10: Complex regex constraint def test_complex_regex_constraint(): trace = Trace(["A", "X", "B", "Y", "C"]) @@ -102,6 +105,7 @@ def test_constraint_not_covered(): 0 ], "The constraint should not be activated by the trace." + # Test 12: Empty trace and constraints def test_empty_trace_and_constraints(): trace = Trace([]) @@ -121,6 +125,7 @@ def test_remove_nonexistent_constraint(): len(explainer.constraints) == 1 ), "Removing a non-existent constraint should not change the constraints list." + # Test 14: Activation with no constraints def test_activation_with_no_constraints(): trace = Trace(["A", "B", "C"]) @@ -219,6 +224,7 @@ def test_complex_counterfactual_explanation(): == "\nAddition (Added B at position 1): A->B->C->E->D" ) + # Test 20: Event logs def test_event_log(): event_log = EventLog() From 0397671e93f96bc3e4bb01f7ace73ab7807b5311 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Fri, 22 Mar 2024 13:29:53 +0100 Subject: [PATCH 19/54] Linting --- tests/explainer/explainer_test.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 0d95126..de4bbe9 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -89,7 +89,7 @@ def test_complex_regex_constraint(): trace = Trace(["A", "X", "B", "Y", "C"]) explainer = Explainer() explainer.add_constraint( - "A.*X.*B.*Y.*C" + "A.*X.*B.*Y.*C" ) # Specifically expects certain nodes in order assert explainer.conformant( trace @@ -161,8 +161,10 @@ def test_conformant_trace_handled_correctly(): explainer = Explainer() explainer.add_constraint("AB") - assert explainer.minimal_expl(trace) == "The trace is already conformant, no changes needed." - + assert ( + explainer.minimal_expl(trace) + == "The trace is already conformant, no changes needed." + ) # Test 17: Conformant trace def test_explainer_methods(): @@ -188,8 +190,8 @@ def test_explainer_methods(): def test_explaination(): explainer = Explainer() - conformant_trace = Trace(["A","B","C"]) - non_conformant_trace = Trace(["A","C"]) + conformant_trace = Trace(["A", "B", "C"]) + non_conformant_trace = Trace(["A", "C"]) explainer.add_constraint("A.*B.*C") @@ -220,8 +222,8 @@ def test_complex_counterfactual_explanation(): counterfactual_explanation = explainer.counterfactual_expl(non_conformant_trace) assert ( - counterfactual_explanation - == "\nAddition (Added B at position 1): A->B->C->E->D" + counterfactual_explanation + == "\nAddition (Added B at position 1): A->B->C->E->D" ) From 813097decdbbfdf5cef5fce3bdafeeceb79a2ef8 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 25 Mar 2024 09:53:26 +0100 Subject: [PATCH 20/54] Linting --- tests/explainer/explainer_test.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index de4bbe9..e22fb32 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -155,7 +155,7 @@ def test_trace_conformance_against_multiple_constraints(): ), "Trace2 should be conformant as it satisfies all constraints." -# Test 16: Conformant trace does not generate minimal explaination +# Test 16: Conformant trace does not generate minimal explaination def test_conformant_trace_handled_correctly(): trace = Trace(["A", "B"]) explainer = Explainer() @@ -166,6 +166,7 @@ def test_conformant_trace_handled_correctly(): == "The trace is already conformant, no changes needed." ) + # Test 17: Conformant trace def test_explainer_methods(): trace = Trace(["A", "B", "C"]) @@ -189,10 +190,10 @@ def test_explainer_methods(): # Test 18: Some explaination test def test_explaination(): explainer = Explainer() - + conformant_trace = Trace(["A", "B", "C"]) non_conformant_trace = Trace(["A", "C"]) - + explainer.add_constraint("A.*B.*C") assert explainer.conformant(non_conformant_trace) == False @@ -212,6 +213,8 @@ def test_explaination(): This part is not very complex as of now and is very much up for change, the complexity of counterfactuals proved to be slightly larger than expected """ + + def test_complex_counterfactual_explanation(): explainer = Explainer() @@ -220,7 +223,7 @@ def test_complex_counterfactual_explanation(): non_conformant_trace = Trace(["A", "C", "E", "D"]) counterfactual_explanation = explainer.counterfactual_expl(non_conformant_trace) - + assert ( counterfactual_explanation == "\nAddition (Added B at position 1): A->B->C->E->D" @@ -256,4 +259,3 @@ def test_event_log(): ("A", "B", "C"): 5, ("X", "Y", "Z"): 7, } # There should be several traces in the log - From f5ef8b7107ef6b8203681726668c7cb0dc6d7cfe Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 25 Mar 2024 09:59:47 +0100 Subject: [PATCH 21/54] linter --- explainer/explainer.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index ba40cf9..3d0fa5f 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -2,19 +2,22 @@ import re from itertools import combinations, product, chain + class Trace: def __init__(self, nodes): """ Initializes a Trace instance. - + :param nodes: A list of nodes where each node is represented as a string label. """ self.nodes = nodes + def __len__(self): """ Returns the number of nodes in the trace. """ return len(self.nodes) + def __iter__(self): """ Initializes the iteration over the nodes in the trace. @@ -32,6 +35,7 @@ def __next__(self): return result else: raise StopIteration + def __split__(self): """ Splits the nodes of the trace into a list. @@ -42,6 +46,7 @@ def __split__(self): for node in self.nodes: spl.append(node) return spl + class EventLog: def __init__(self, trace=None): """ @@ -53,7 +58,7 @@ def __init__(self, trace=None): if trace: self.add_trace(trace) - def add_trace(self, trace, count = 1): + def add_trace(self, trace, count=1): """ Adds a trace to the log or increments its count if it already exists. @@ -65,7 +70,7 @@ def add_trace(self, trace, count = 1): else: self.log[trace_tuple] = count - def remove_trace(self, trace, count = 1): + def remove_trace(self, trace, count=1): """ Removes a trace from the log or decrements its count if the count is greater than 1. @@ -136,7 +141,7 @@ def remove_constraint(self, idx): if node not in remaining_nodes: self.nodes.discard(node) - def activation(self, trace, constraints = None): + def activation(self, trace, constraints=None): """ Checks if any of the nodes in the trace activates any constraint. @@ -184,7 +189,7 @@ def identify_existance_constraints(self, pattern): return None - def conformant(self, trace, constraints = None): + def conformant(self, trace, constraints=None): """ Checks if the trace is conformant according to all the constraints. @@ -275,7 +280,7 @@ def counterfactual_expl(self, trace): return self.operate_on_trace(trace, score, '') - def counter_factual_helper(self, working_trace, explanation, depth = 0): + def counter_factual_helper(self, working_trace, explanation, depth=0): """ Recursively explores counterfactual explanations for a working trace. @@ -292,7 +297,7 @@ def counter_factual_helper(self, working_trace, explanation, depth = 0): return self.operate_on_trace(working_trace, score, explanation, depth) - def operate_on_trace(self, trace, score, explanation_path, depth = 0): + def operate_on_trace(self, trace, score, explanation_path, depth=0): """ Finds and applies modifications to the trace to make it conformant. @@ -318,7 +323,7 @@ def operate_on_trace(self, trace, score, explanation_path, depth = 0): explanation_string = explanation_path + '\n' + str(explanation) return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) - def get_nodes_from_constraint(self, constraint = None): + def get_nodes_from_constraint(self, constraint=None): """ Extracts unique nodes from a constraint pattern. @@ -407,7 +412,7 @@ def evaluate_similarity(self, trace): normalized_score = 1 - lev_distance / max_distance return normalized_score - def determine_conformance_rate(self, event_log, constraints = None): + def determine_conformance_rate(self, event_log, constraints=None): """ Determines the conformance rate of the event log based on the given constraints. @@ -430,7 +435,7 @@ def determine_conformance_rate(self, event_log, constraints = None): break return (len_log - non_conformant) / len_log - def trace_contribution_to_conformance_loss(self, event_log, trace, constraints = None): + def trace_contribution_to_conformance_loss(self, event_log, trace, constraints=None): """ Calculates the contribution of a specific trace to the conformance loss of the event log. From e760cde179a9abcb26f450e4413772fc93fecd4a Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 25 Mar 2024 10:12:32 +0100 Subject: [PATCH 22/54] Linter --- explainer/explainer.py | 166 +++++++++++++++++++++++------------------ 1 file changed, 93 insertions(+), 73 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 3d0fa5f..993a5bc 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -17,7 +17,7 @@ def __len__(self): Returns the number of nodes in the trace. """ return len(self.nodes) - + def __iter__(self): """ Initializes the iteration over the nodes in the trace. @@ -47,6 +47,7 @@ def __split__(self): spl.append(node) return spl + class EventLog: def __init__(self, trace=None): """ @@ -88,7 +89,7 @@ def __str__(self): Returns a string representation of the event log. """ return str(self.log) - + def __len__(self): """ Returns the total number of trace occurrences in the log. @@ -103,6 +104,7 @@ def __iter__(self): for _ in range(count): yield Trace(list(trace_tuple)) + class Explainer: def __init__(self): """ @@ -110,11 +112,11 @@ def __init__(self): """ self.constraints = [] # List to store constraints (regex patterns) self.adherent_trace = None - + def add_constraint(self, regex): """ Adds a new constraint and updates the nodes list. - + :param regex: A regular expression representing the constraint. """ self.constraints.append(regex) @@ -125,15 +127,15 @@ def add_constraint(self, regex): def remove_constraint(self, idx): """ Removes a constraint by index and updates the nodes list if necessary. - + :param idx: Index of the constraint to be removed. """ if 0 <= idx < len(self.constraints): removed_regex = self.constraints.pop(idx) removed_nodes = set(filter(str.isalpha, removed_regex)) - + # Re-evaluate nodes to keep based on remaining constraints - remaining_nodes = set(filter(str.isalpha, ''.join(self.constraints))) + remaining_nodes = set(filter(str.isalpha, "".join(self.constraints))) self.nodes = remaining_nodes # Optionally, remove nodes that are no longer in any constraint @@ -144,7 +146,7 @@ def remove_constraint(self, idx): def activation(self, trace, constraints=None): """ Checks if any of the nodes in the trace activates any constraint. - + :param trace: A Trace instance. :return: Boolean indicating if any constraint is activated. """ @@ -161,10 +163,10 @@ def activation(self, trace, constraints=None): con_activation[idx] = 1 continue for event in trace: - if event in con: - con_activation[idx] = 1 - activated = True - break + if event in con: + con_activation[idx] = 1 + activated = True + break return con_activation def identify_existance_constraints(self, pattern): @@ -175,24 +177,24 @@ def identify_existance_constraints(self, pattern): :return: A tuple indicating the type of existance constraint and the node involved. """ # Check for AtLeastOne constraint - for match in re.finditer(r'(? 100: - return f'{explanation}\n Maximum depth of {depth -1} reached' + return f"{explanation}\n Maximum depth of {depth -1} reached" score = self.evaluate_similarity(working_trace) return self.operate_on_trace(working_trace, score, explanation, depth) - def operate_on_trace(self, trace, score, explanation_path, depth=0): """ Finds and applies modifications to the trace to make it conformant. @@ -310,7 +312,7 @@ def operate_on_trace(self, trace, score, explanation_path, depth=0): explanation = None counter_factuals = self.modify_subtrace(trace) best_subtrace = None - best_score = -float('inf') + best_score = -float("inf") for subtrace in counter_factuals: current_score = self.evaluate_similarity(subtrace[0]) if current_score > best_score and current_score > score: @@ -320,28 +322,28 @@ def operate_on_trace(self, trace, score, explanation_path, depth=0): if best_subtrace == None: for subtrace in counter_factuals: self.operate_on_trace(subtrace[0], score, explanation_path, depth + 1) - explanation_string = explanation_path + '\n' + str(explanation) + explanation_string = explanation_path + "\n" + str(explanation) return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) def get_nodes_from_constraint(self, constraint=None): """ Extracts unique nodes from a constraint pattern. - + :param constraint: The constraint pattern as a string. :return: A list of unique nodes found within the constraint. """ if constraint is None: all_nodes = set() - for constr in self.constraints: - all_nodes.update(re.findall(r'[A-Za-z]', constr)) + for con in self.constraints: + all_nodes.update(re.findall(r"[A-Za-z]", con)) return list(set(all_nodes)) else: - return list(set(re.findall(r'[A-Za-z]', constraint))) - + return list(set(re.findall(r"[A-Za-z]", constraint))) + def modify_subtrace(self, trace): """ Modifies the given trace to meet constraints by adding nodes where the pattern fails. - + Parameters: - trace: A list of node identifiers @@ -349,25 +351,35 @@ def modify_subtrace(self, trace): - A list of potential subtraces each modified to meet constraints. """ potential_subtraces = [] - possible_additions = self.get_nodes_from_constraint() + possible_additions = self.get_nodes_from_constraint() for i, s_trace in enumerate(get_iterative_subtrace(trace)): for con in self.constraints: new_trace_str = "".join(s_trace) match = re.match(new_trace_str, con) if not match: for add in possible_additions: - - potential_subtraces.append([Trace(s_trace + [add] + trace.nodes[i+1:]), - f"Addition (Added {add} at position {i+1}): " + "->" - .join(s_trace + [add] + trace.nodes[i+1:])]) - potential_subtraces.append([Trace(s_trace[:-1] + [add] + trace.nodes[i:]), - f"Addition (Added {add} at position {i}): " + "->" - .join(s_trace[:-1] + [add] + trace.nodes[i:])]) - - potential_subtraces.append([Trace(s_trace[:-1] + trace.nodes[i+1:]), - f"Subtraction (Removed {s_trace[i]} from position {i}): " + "->". - join(s_trace[:-1] + trace.nodes[i+1:])]) - + potential_subtraces.append( + [ + Trace(s_trace + [add] + trace.nodes[i + 1 :]), + f"Addition (Added {add} at position {i+1}): " + + "->".join(s_trace + [add] + trace.nodes[i + 1 :]), + ] + ) + potential_subtraces.append( + [ + Trace(s_trace[:-1] + [add] + trace.nodes[i:]), + f"Addition (Added {add} at position {i}): " + + "->".join(s_trace[:-1] + [add] + trace.nodes[i:]), + ] + ) + + potential_subtraces.append( + [ + Trace(s_trace[:-1] + trace.nodes[i + 1 :]), + f"Subtraction (Removed {s_trace[i]} from position {i}): " + + "->".join(s_trace[:-1] + trace.nodes[i + 1 :]), + ] + ) return potential_subtraces def determine_shapley_value(self, log, constraints, index): @@ -383,8 +395,7 @@ def determine_shapley_value(self, log, constraints, index): rate """ if len(constraints) < index: - raise Exception ( - 'Constraint not in constraint list.') + raise Exception ("Constraint not in constraint list.") contributor = constraints[index] sub_ctrbs = [] reduced_constraints = [c for c in constraints if not c == contributor] @@ -393,11 +404,17 @@ def determine_shapley_value(self, log, constraints, index): lsubset = list(subset) constraints_without = [c for c in constraints if c in lsubset] constraints_with = [c for c in constraints if c in lsubset + [contributor]] - weight = (math.factorial(len(lsubset)) * math.factorial(len(constraints)-1-len(lsubset)))/math.factorial(len(constraints)) - sub_ctrb = weight * (self.determine_conformance_rate(log, constraints_without) - self.determine_conformance_rate(log, constraints_with)) + weight = ( + math.factorial(len(lsubset)) + * math.factorial(len(constraints) - 1 - len(lsubset)) + ) / math.factorial(len(constraints)) + sub_ctrb = weight * ( + self.determine_conformance_rate(log, constraints_without) + - self.determine_conformance_rate(log, constraints_with) + ) sub_ctrbs.append(sub_ctrb) return sum(sub_ctrbs) - + def evaluate_similarity(self, trace): """ Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance. @@ -411,7 +428,7 @@ def evaluate_similarity(self, trace): max_distance = max(length, trace_len) normalized_score = 1 - lev_distance / max_distance return normalized_score - + def determine_conformance_rate(self, event_log, constraints=None): """ Determines the conformance rate of the event log based on the given constraints. @@ -434,8 +451,10 @@ def determine_conformance_rate(self, event_log, constraints=None): non_conformant += count break return (len_log - non_conformant) / len_log - - def trace_contribution_to_conformance_loss(self, event_log, trace, constraints=None): + + def trace_contribution_to_conformance_loss( + self, event_log, trace, constraints=None + ): """ Calculates the contribution of a specific trace to the conformance loss of the event log. @@ -448,13 +467,13 @@ def trace_contribution_to_conformance_loss(self, event_log, trace, constraints=N constraints = self.constraints total_traces = len(event_log) contribution_of_trace = 0 - for t, count in event_log.log.items(): + for t, count in event_log.log.items(): if not self.conformant(t, constraints): if trace.nodes == list(t): contribution_of_trace = count return contribution_of_trace / total_traces - + def determine_powerset(elements): """Determines the powerset of a list of elements Args: @@ -463,13 +482,15 @@ def determine_powerset(elements): list: Powerset of elements """ lset = list(elements) - ps_elements = chain.from_iterable(combinations(lset, option) for option in range(len(lset) + 1)) + ps_elements = chain.from_iterable( + combinations(lset, option) for option in range(len(lset) + 1) + ) return [set(ps_element) for ps_element in ps_elements] def get_sublists(lst): """ Generates all possible non-empty sublists of a list. - + :param lst: The input list. :return: A list of all non-empty sublists. """ @@ -477,10 +498,11 @@ def get_sublists(lst): for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n sublists.extend(combinations(lst, r)) return sublists + def get_iterative_subtrace(trace): """ Generates all possible non-empty contiguous sublists of a list, maintaining order. - + :param lst: The input list. n: the minmum length of sublists :return: A list of all non-empty contiguous sublists. @@ -494,11 +516,11 @@ def get_iterative_subtrace(trace): def levenshtein_distance(seq1, seq2): """ Calculates the Levenshtein distance between two sequences. - + Args: seq1 (str): The first sequence. seq2 (str): The second sequence. - + Returns: int: The Levenshtein distance between the two sequences. """ @@ -512,12 +534,10 @@ def levenshtein_distance(seq1, seq2): for x in range(1, size_x): for y in range(1, size_y): - if seq1[x-1] == seq2[y-1]: - matrix[x][y] = matrix[x-1][y-1] + if seq1[x - 1] == seq2[y - 1]: + matrix[x][y] = matrix[x - 1][y - 1] else: matrix[x][y] = min( - matrix[x-1][y] + 1, - matrix[x][y-1] + 1, - matrix[x-1][y-1] + 1 + matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 ) - return matrix[size_x-1][size_y-1] \ No newline at end of file + return matrix[size_x-1][size_y-1] From 3f11afd45884e7c1337391e45333747c41399619 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 25 Mar 2024 10:15:31 +0100 Subject: [PATCH 23/54] Linting --- explainer/explainer.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 993a5bc..ffb2811 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -189,8 +189,7 @@ def identify_existance_constraints(self, pattern): if init_match: return ("I", f"{init_match.group(1)}") return None - - + def conformant(self, trace, constraints=None): """ Checks if the trace is conformant according to all the constraints. @@ -234,7 +233,7 @@ def minimal_expl(self, trace): :param trace: A Trace instance. :return: Explanation of why the trace is non-conformant. """ - + # Because constraints that are not activated should not be considered we create a new explainer with the relevant constraints in this case activation = self.activation(trace) if any(value == 0 for value in activation): @@ -243,11 +242,11 @@ def minimal_expl(self, trace): if value == 1: new_explainer.add_constraint(self.constraints[idx]) return new_explainer.minimal_expl(trace) - + if self.conformant(trace): return "The trace is already conformant, no changes needed." explanations = None - + for constraint in self.constraints: for subtrace in get_sublists(trace): trace_str = "".join(subtrace) @@ -343,9 +342,9 @@ def get_nodes_from_constraint(self, constraint=None): def modify_subtrace(self, trace): """ Modifies the given trace to meet constraints by adding nodes where the pattern fails. - + Parameters: - - trace: A list of node identifiers + - trace: A list of node identifiers Returns: - A list of potential subtraces each modified to meet constraints. @@ -395,7 +394,7 @@ def determine_shapley_value(self, log, constraints, index): rate """ if len(constraints) < index: - raise Exception ("Constraint not in constraint list.") + raise Exception("Constraint not in constraint list.") contributor = constraints[index] sub_ctrbs = [] reduced_constraints = [c for c in constraints if not c == contributor] @@ -454,7 +453,7 @@ def determine_conformance_rate(self, event_log, constraints=None): def trace_contribution_to_conformance_loss( self, event_log, trace, constraints=None - ): + ): """ Calculates the contribution of a specific trace to the conformance loss of the event log. @@ -474,6 +473,7 @@ def trace_contribution_to_conformance_loss( return contribution_of_trace / total_traces + def determine_powerset(elements): """Determines the powerset of a list of elements Args: @@ -487,6 +487,7 @@ def determine_powerset(elements): ) return [set(ps_element) for ps_element in ps_elements] + def get_sublists(lst): """ Generates all possible non-empty sublists of a list. @@ -499,6 +500,7 @@ def get_sublists(lst): sublists.extend(combinations(lst, r)) return sublists + def get_iterative_subtrace(trace): """ Generates all possible non-empty contiguous sublists of a list, maintaining order. @@ -509,10 +511,11 @@ def get_iterative_subtrace(trace): """ sublists = [] for i in range(0, len(trace)): - sublists.append(trace.nodes[0:i+1]) - + sublists.append(trace.nodes[0 : i + 1]) + return sublists + def levenshtein_distance(seq1, seq2): """ Calculates the Levenshtein distance between two sequences. @@ -540,4 +543,4 @@ def levenshtein_distance(seq1, seq2): matrix[x][y] = min( matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 ) - return matrix[size_x-1][size_y-1] + return matrix[size_x - 1][size_y - 1] From 7e2c83620a9291e1d0afd9818644b59fe61f6081 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 3 Apr 2024 10:54:52 +0200 Subject: [PATCH 24/54] Added minimal solution feature --- explainer/explainer.py | 41 ++++++-- explainer/tutorial/explainer_tutorial.ipynb | 105 +++++++++++++++++--- 2 files changed, 122 insertions(+), 24 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index ffb2811..3d4bb75 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -112,7 +112,19 @@ def __init__(self): """ self.constraints = [] # List to store constraints (regex patterns) self.adherent_trace = None + self.adherent_traces = [] + self.minimal_solution = False + + def set_minimal_solution(self, minimal_solution): + """ + Tells the explainer to generate minimal solutions + Note: This will increase computations significantly + Args: + minimal_solution (bool): True to generate minimal solutions, False if it should be the first possible + """ + self.minimal_solution = minimal_solution + def add_constraint(self, regex): """ Adds a new constraint and updates the nodes list. @@ -209,22 +221,24 @@ def conformant(self, trace, constraints=None): return all(re.search(constraint, trace_str) for constraint in constraints) return all(re.search(constraint, trace_str) for constraint in self.constraints) - def contradiction(self): + def contradiction(self, check_multiple = False, max_length = 10): """ Checks if there is a contradiction among the constraints. :return: Boolean indicating if there is a contradiction. """ nodes = self.get_nodes_from_constraint() - max_length = 10 # Set a reasonable max length to avoid infinite loops nodes = nodes + nodes for length in range(1, max_length + 1): for combination in product(nodes, repeat=length): test_str = "".join(combination) if all(re.search(con, test_str) for con in self.constraints): - self.adherent_trace = test_str - return False # Found a match - return True # No combination satisfied all constraints + if not check_multiple: # Standard, if solution is not minimal + self.adherent_trace = test_str + return False # Found a match + else: + self.adherent_traces.append(test_str) + return not check_multiple # No combination satisfied all constraints def minimal_expl(self, trace): """ @@ -275,7 +289,8 @@ def counterfactual_expl(self, trace): if value == 1: new_explainer.add_constraint(self.constraints[idx]) return new_explainer.counterfactual_expl(trace) - + if self.minimal_solution: + self.contradiction(True, len(trace) + 1) # If the solution should be minimal, calculate all possible solutions if self.conformant(trace): return "The trace is already conformant, no changes needed." score = self.evaluate_similarity(trace) @@ -421,9 +436,19 @@ def evaluate_similarity(self, trace): :param trace: The trace to compare with the adherent trace. :return: A normalized score indicating the similarity between the adherent trace and the given trace. """ - length = len(self.adherent_trace) + length = 0 trace_len = len("".join(trace)) - lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) + lev_distance = 0 + if self.minimal_solution: + lev_distance = len(max(self.adherent_traces)) + trace_len # The maximum possible levenshtein distance + length = len(max(self.adherent_traces)) + for t in self.adherent_traces: + tmp_dist = levenshtein_distance(t, "".join(trace)) + if lev_distance > tmp_dist: # Closer to a possible solution + lev_distance = tmp_dist + else: + length = len(self.adherent_trace) + lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) max_distance = max(length, trace_len) normalized_score = 1 - lev_distance / max_distance return normalized_score diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index c8c0504..8dfb5f3 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -95,32 +95,36 @@ "text": [ "Constraint: A.*B.*C\n", "Trace:['A', 'C']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", "\n", "Addition (Added B at position 1): A->B->C\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['C', 'B', 'A']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", "\n", "Addition (Added A at position 1): C->A->B->A\n", "Subtraction (Removed C from position 0): A->B->A\n", "Addition (Added C at position 2): A->B->C->A\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'A', 'C']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", "\n", "Addition (Added B at position 2): A->A->B->C\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", "-----------\n", "Constraint: A.*B.*C\n", "Trace:['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", + "\n", + "Subtraction (Removed TEST from position 4): A->A->C->A->A->C->X->Y\n", + "Addition (Added B at position 2): A->A->B->C->A->A->C->X->Y\n", "-----------\n", "Constraint: AC\n", "Trace:['A', 'X', 'C']\n", + "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", "\n", "Subtraction (Removed X from position 1): A->C\n", - "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", "-----------\n", "constraint: AC\n", "constraint: B.*A.*B.*C\n", @@ -129,9 +133,9 @@ "constraint: A[^D]*B\n", "constraint: B.*[^X].*\n", "Trace:['A', 'X', 'C']\n", + "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", "\n", - "Subtraction (Removed X from position 1): A->C\n", - "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n" + "Subtraction (Removed X from position 1): A->C\n" ] } ], @@ -139,30 +143,30 @@ "non_conformant_trace = Trace(['A', 'C'])\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", "\n", "non_conformant_trace = Trace(['C', 'B', 'A'])\n", "print('-----------')\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", "\n", "non_conformant_trace = Trace(['A','A','C'])\n", "print('-----------')\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", "\n", "\n", "non_conformant_trace = Trace(['A','A','C','A','TEST','A','C', 'X', 'Y']) \n", "print('-----------')\n", "print('Constraint: A.*B.*C')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "#print(explainer.counterfactual_expl(non_conformant_trace))\n", - "#print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", "\n", "\n", "explainer.remove_constraint(0)\n", @@ -171,8 +175,8 @@ "print('-----------')\n", "print('Constraint: AC')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print('-----------')\n", "\n", "explainer.add_constraint('B.*A.*B.*C')\n", @@ -184,8 +188,8 @@ "for con in explainer.constraints:\n", " print(f'constraint: {con}')\n", "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", "\n", "\n" ] @@ -194,14 +198,83 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 5: Event Logs and Shapely values\n", + "## Step 5: Generating minimal solutions" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Example without minimal solution\n", + "--------------------------------\n", + "\n", + "Subtraction (Removed A from position 2): A->B->C->B\n", + "Subtraction (Removed B from position 3): A->B->C\n", + "\n", + "Example with minimal solution\n", + "--------------------------------\n", + "\n", + "Addition (Added C at position 5): A->B->A->C->B->C\n", + "\n", + "Example without minimal solution\n", + "--------------------------------\n", + "\n", + "Addition (Added A at position 1): C->A->B->A\n", + "Subtraction (Removed C from position 0): A->B->A\n", + "Addition (Added C at position 2): A->B->C->A\n", + "Subtraction (Removed A from position 3): A->B->C\n", + "\n", + "Example with minimal solution\n", + "--------------------------------\n", + "\n", + "Addition (Added A at position 0): A->C->B->A\n", + "Addition (Added C at position 4): A->C->B->A->C\n" + ] + } + ], + "source": [ + "exp = Explainer()\n", + "exp.add_constraint(\"^A\")\n", + "exp.add_constraint(\"A.*B.*\")\n", + "exp.add_constraint(\"C$\")\n", + "trace = Trace(['A', 'B','A','C', 'B'])\n", + "print(\"Example without minimal solution\")\n", + "print(\"--------------------------------\")\n", + "print(exp.counterfactual_expl(trace))\n", + "\n", + "print(\"\\nExample with minimal solution\")\n", + "print(\"--------------------------------\")\n", + "exp.set_minimal_solution(True)\n", + "print(exp.counterfactual_expl(trace))\n", + "exp.set_minimal_solution(False)\n", + "trace = Trace(['C','B','A'])\n", + "print(\"\\nExample without minimal solution\")\n", + "print(\"--------------------------------\")\n", + "print(exp.counterfactual_expl(trace))\n", + "\n", + "print(\"\\nExample with minimal solution\")\n", + "print(\"--------------------------------\")\n", + "exp.set_minimal_solution(True)\n", + "print(exp.counterfactual_expl(trace))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 6: Event Logs and Shapely values\n", "\n", "The event logs in this context is built with traces, here's how you set them up." ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -239,7 +312,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { From acda6a4bf51f25770c9c13765720f0491f822791 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 3 Apr 2024 12:56:01 +0200 Subject: [PATCH 25/54] Linting --- explainer/explainer.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 3d4bb75..a5b81ae 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -114,7 +114,7 @@ def __init__(self): self.adherent_trace = None self.adherent_traces = [] self.minimal_solution = False - + def set_minimal_solution(self, minimal_solution): """ Tells the explainer to generate minimal solutions @@ -124,7 +124,7 @@ def set_minimal_solution(self, minimal_solution): minimal_solution (bool): True to generate minimal solutions, False if it should be the first possible """ self.minimal_solution = minimal_solution - + def add_constraint(self, regex): """ Adds a new constraint and updates the nodes list. @@ -221,7 +221,7 @@ def conformant(self, trace, constraints=None): return all(re.search(constraint, trace_str) for constraint in constraints) return all(re.search(constraint, trace_str) for constraint in self.constraints) - def contradiction(self, check_multiple = False, max_length = 10): + def contradiction(self, check_multiple=False, max_length=10): """ Checks if there is a contradiction among the constraints. @@ -233,7 +233,7 @@ def contradiction(self, check_multiple = False, max_length = 10): for combination in product(nodes, repeat=length): test_str = "".join(combination) if all(re.search(con, test_str) for con in self.constraints): - if not check_multiple: # Standard, if solution is not minimal + if not check_multiple: # Standard, if solution is not minimal self.adherent_trace = test_str return False # Found a match else: @@ -290,7 +290,10 @@ def counterfactual_expl(self, trace): new_explainer.add_constraint(self.constraints[idx]) return new_explainer.counterfactual_expl(trace) if self.minimal_solution: - self.contradiction(True, len(trace) + 1) # If the solution should be minimal, calculate all possible solutions + self.contradiction( + True, + len(trace) + 1 + ) # If the solution should be minimal, calculate all possible solutions if self.conformant(trace): return "The trace is already conformant, no changes needed." score = self.evaluate_similarity(trace) @@ -436,15 +439,17 @@ def evaluate_similarity(self, trace): :param trace: The trace to compare with the adherent trace. :return: A normalized score indicating the similarity between the adherent trace and the given trace. """ - length = 0 + length = 0 trace_len = len("".join(trace)) - lev_distance = 0 + lev_distance = 0 if self.minimal_solution: - lev_distance = len(max(self.adherent_traces)) + trace_len # The maximum possible levenshtein distance + lev_distance = ( + len(max(self.adherent_traces)) + trace_len + )# The maximum possible levenshtein distance length = len(max(self.adherent_traces)) for t in self.adherent_traces: tmp_dist = levenshtein_distance(t, "".join(trace)) - if lev_distance > tmp_dist: # Closer to a possible solution + if lev_distance > tmp_dist: # Closer to a possible solution lev_distance = tmp_dist else: length = len(self.adherent_trace) From f3bd947040994e2b4cc6a545328f569e35ca8dce Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 3 Apr 2024 12:58:33 +0200 Subject: [PATCH 26/54] Linter --- explainer/explainer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index a5b81ae..72b0986 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -113,7 +113,7 @@ def __init__(self): self.constraints = [] # List to store constraints (regex patterns) self.adherent_trace = None self.adherent_traces = [] - self.minimal_solution = False + self.minimal_solution = False def set_minimal_solution(self, minimal_solution): """ @@ -291,9 +291,8 @@ def counterfactual_expl(self, trace): return new_explainer.counterfactual_expl(trace) if self.minimal_solution: self.contradiction( - True, - len(trace) + 1 - ) # If the solution should be minimal, calculate all possible solutions + True, len(trace) + 1 + ) # If the solution should be minimal, calculate all possible solutions if self.conformant(trace): return "The trace is already conformant, no changes needed." score = self.evaluate_similarity(trace) @@ -444,8 +443,8 @@ def evaluate_similarity(self, trace): lev_distance = 0 if self.minimal_solution: lev_distance = ( - len(max(self.adherent_traces)) + trace_len - )# The maximum possible levenshtein distance + len(max(self.adherent_traces)) + trace_len + ) # The maximum possible levenshtein distance length = len(max(self.adherent_traces)) for t in self.adherent_traces: tmp_dist = levenshtein_distance(t, "".join(trace)) From 6288107246966f86847cb1cb024b46a3360d9089 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 3 Apr 2024 12:59:30 +0200 Subject: [PATCH 27/54] Linter --- explainer/explainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 72b0986..1db1365 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -292,7 +292,7 @@ def counterfactual_expl(self, trace): if self.minimal_solution: self.contradiction( True, len(trace) + 1 - ) # If the solution should be minimal, calculate all possible solutions + ) # If the solution should be minimal, calculate all possible solutions if self.conformant(trace): return "The trace is already conformant, no changes needed." score = self.evaluate_similarity(trace) From 11df388f33299fb5263de59b8167504b2d96d4af Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 23 Apr 2024 14:36:49 +0200 Subject: [PATCH 28/54] Updated explainer --- .gitignore | 3 +- explainer/explainer.py | 589 ++++---------------- explainer/explainer_regex.py | 511 +++++++++++++++++ explainer/explainer_signal.py | 69 +++ explainer/tutorial/explainer_tutorial.ipynb | 273 ++++++--- tests/explainer/explainer_test.py | 40 +- 6 files changed, 922 insertions(+), 563 deletions(-) create mode 100644 explainer/explainer_regex.py create mode 100644 explainer/explainer_signal.py diff --git a/.gitignore b/.gitignore index d34c252..a5dc1ba 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,5 @@ dmypy.json # explainer Stuff explainer/test.py -explainer/old_code.py \ No newline at end of file +explainer/old_code.py +explainer/conf.py \ No newline at end of file diff --git a/explainer/explainer.py b/explainer/explainer.py index 1db1365..3744d14 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -1,8 +1,110 @@ -import math -import re -from itertools import combinations, product, chain +from abc import ABC, abstractmethod +class Explainer(ABC): + def __init__(self): + """ + Initializes an Explainer instance. + """ + self.constraints = [] # List to store constraints (constraint patterns) + self.adherent_trace = None + self.adherent_traces = [] + self.minimal_solution = False + def set_minimal_solution(self, minimal_solution): + """ + Tells the explainer to generate minimal solutions + Note: This will increase computations significantly + + Args: + minimal_solution (bool): True to generate minimal solutions, False if it should be the first possible + """ + self.minimal_solution = minimal_solution + + def add_constraint(self, constr): + """ + Adds a new constraint and updates the nodes list. + + :param constr: A regular expression or Signal constrain representing the constraint. + """ + self.constraints.append(constr) + if self.contradiction(): + self.constraints.remove(constr) + print(f"Constraint {constr} contradicts the other constraints.") + + # Marking remove_constraint as abstract as an example + @abstractmethod + def remove_constraint(self, idx): + """ + Removes a constraint by index and updates the nodes list if necessary. + + :param idx: Index of the constraint to be removed. + """ + pass + + # Marking activation as abstract as an example + @abstractmethod + def activation(self, trace, constraints=None): + """ + Checks if any of the nodes in the trace activates any constraint. + + :param trace: A Trace instance. + :return: Boolean indicating if any constraint is activated. + """ + pass + + @abstractmethod + def conformant(self, trace, constraints=None): + # Implementation remains the same + pass + + @abstractmethod + def contradiction(self, check_multiple=False, max_length=10): + # Implementation remains the same + pass + + @abstractmethod + def minimal_expl(self, trace): + # Implementation remains the same + pass + + @abstractmethod + def counterfactual_expl(self, trace): + # Implementation remains the same + pass + + @abstractmethod + def evaluate_similarity(self, trace): + # Implementation remains the same + pass + + @abstractmethod + def determine_conformance_rate(self, event_log, constraints=None): + # Implementation remains the same + pass + + @abstractmethod + def determine_fitness_rate(self, event_log, constraints = None): + pass + + @abstractmethod + def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): + # Implementation remains the same + pass + + @abstractmethod + def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): + # Implementation remains the same + pass + + @abstractmethod + def constraint_ctrb_to_conformance(self, log, constraints, index): + pass + + @abstractmethod + def constraint_ctrb_to_fitness(self, log, constraints, index): + # Implementation remains the same + pass + class Trace: def __init__(self, nodes): """ @@ -46,8 +148,7 @@ def __split__(self): for node in self.nodes: spl.append(node) return spl - - + class EventLog: def __init__(self, trace=None): """ @@ -83,7 +184,16 @@ def remove_trace(self, trace, count=1): self.log[trace_tuple] -= count else: del self.log[trace_tuple] + + def get_variant_count(self, trace): + """ + Returns the count of the specified trace in the log. + :param trace: A Trace instance to check. + """ + trace_tuple = tuple(trace.nodes) + return self.log.get(trace_tuple, 0) + def __str__(self): """ Returns a string representation of the event log. @@ -104,472 +214,3 @@ def __iter__(self): for _ in range(count): yield Trace(list(trace_tuple)) - -class Explainer: - def __init__(self): - """ - Initializes an Explainer instance. - """ - self.constraints = [] # List to store constraints (regex patterns) - self.adherent_trace = None - self.adherent_traces = [] - self.minimal_solution = False - - def set_minimal_solution(self, minimal_solution): - """ - Tells the explainer to generate minimal solutions - Note: This will increase computations significantly - - Args: - minimal_solution (bool): True to generate minimal solutions, False if it should be the first possible - """ - self.minimal_solution = minimal_solution - - def add_constraint(self, regex): - """ - Adds a new constraint and updates the nodes list. - - :param regex: A regular expression representing the constraint. - """ - self.constraints.append(regex) - if self.contradiction(): - self.constraints.remove(regex) - print(f"Constraint {regex} contradicts the other constraints.") - - def remove_constraint(self, idx): - """ - Removes a constraint by index and updates the nodes list if necessary. - - :param idx: Index of the constraint to be removed. - """ - if 0 <= idx < len(self.constraints): - removed_regex = self.constraints.pop(idx) - removed_nodes = set(filter(str.isalpha, removed_regex)) - - # Re-evaluate nodes to keep based on remaining constraints - remaining_nodes = set(filter(str.isalpha, "".join(self.constraints))) - self.nodes = remaining_nodes - - # Optionally, remove nodes that are no longer in any constraint - for node in removed_nodes: - if node not in remaining_nodes: - self.nodes.discard(node) - - def activation(self, trace, constraints=None): - """ - Checks if any of the nodes in the trace activates any constraint. - - :param trace: A Trace instance. - :return: Boolean indicating if any constraint is activated. - """ - if not constraints: - constraints = self.constraints - con_activation = [0] * len(constraints) - activated = False - for idx, con in enumerate(constraints): - if activated: - activated = False - continue - target = self.identify_existance_constraints(con) - if target: - con_activation[idx] = 1 - continue - for event in trace: - if event in con: - con_activation[idx] = 1 - activated = True - break - return con_activation - - def identify_existance_constraints(self, pattern): - """ - Identifies existance constraints within a pattern. - - :param pattern: The constraint pattern as a string. - :return: A tuple indicating the type of existance constraint and the node involved. - """ - # Check for AtLeastOne constraint - for match in re.finditer(r"(? 100: - return f"{explanation}\n Maximum depth of {depth -1} reached" - score = self.evaluate_similarity(working_trace) - return self.operate_on_trace(working_trace, score, explanation, depth) - - def operate_on_trace(self, trace, score, explanation_path, depth=0): - """ - Finds and applies modifications to the trace to make it conformant. - - :param trace: The trace to be modified. - :param score: The similarity score of the trace. - :param explanation_path: The current explanation path. - :param depth: The current recursion depth. - :return: A string explaining why the best subtrace is non-conformant or a message indicating the maximum depth has been reached. - """ - explanation = None - counter_factuals = self.modify_subtrace(trace) - best_subtrace = None - best_score = -float("inf") - for subtrace in counter_factuals: - current_score = self.evaluate_similarity(subtrace[0]) - if current_score > best_score and current_score > score: - best_score = current_score - best_subtrace = subtrace[0] - explanation = subtrace[1] - if best_subtrace == None: - for subtrace in counter_factuals: - self.operate_on_trace(subtrace[0], score, explanation_path, depth + 1) - explanation_string = explanation_path + "\n" + str(explanation) - return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) - - def get_nodes_from_constraint(self, constraint=None): - """ - Extracts unique nodes from a constraint pattern. - - :param constraint: The constraint pattern as a string. - :return: A list of unique nodes found within the constraint. - """ - if constraint is None: - all_nodes = set() - for con in self.constraints: - all_nodes.update(re.findall(r"[A-Za-z]", con)) - return list(set(all_nodes)) - else: - return list(set(re.findall(r"[A-Za-z]", constraint))) - - def modify_subtrace(self, trace): - """ - Modifies the given trace to meet constraints by adding nodes where the pattern fails. - - Parameters: - - trace: A list of node identifiers - - Returns: - - A list of potential subtraces each modified to meet constraints. - """ - potential_subtraces = [] - possible_additions = self.get_nodes_from_constraint() - for i, s_trace in enumerate(get_iterative_subtrace(trace)): - for con in self.constraints: - new_trace_str = "".join(s_trace) - match = re.match(new_trace_str, con) - if not match: - for add in possible_additions: - potential_subtraces.append( - [ - Trace(s_trace + [add] + trace.nodes[i + 1 :]), - f"Addition (Added {add} at position {i+1}): " - + "->".join(s_trace + [add] + trace.nodes[i + 1 :]), - ] - ) - potential_subtraces.append( - [ - Trace(s_trace[:-1] + [add] + trace.nodes[i:]), - f"Addition (Added {add} at position {i}): " - + "->".join(s_trace[:-1] + [add] + trace.nodes[i:]), - ] - ) - - potential_subtraces.append( - [ - Trace(s_trace[:-1] + trace.nodes[i + 1 :]), - f"Subtraction (Removed {s_trace[i]} from position {i}): " - + "->".join(s_trace[:-1] + trace.nodes[i + 1 :]), - ] - ) - return potential_subtraces - - def determine_shapley_value(self, log, constraints, index): - """Determines the Shapley value-based contribution of a constraint to a the - overall conformance rate. - Args: - log (dictionary): The event log, where keys are strings and values are - ints - constraints (list): A list of constraints (regexp strings) - index (int): The - Returns: - float: The contribution of the constraint to the overall conformance - rate - """ - if len(constraints) < index: - raise Exception("Constraint not in constraint list.") - contributor = constraints[index] - sub_ctrbs = [] - reduced_constraints = [c for c in constraints if not c == contributor] - subsets = determine_powerset(reduced_constraints) - for subset in subsets: - lsubset = list(subset) - constraints_without = [c for c in constraints if c in lsubset] - constraints_with = [c for c in constraints if c in lsubset + [contributor]] - weight = ( - math.factorial(len(lsubset)) - * math.factorial(len(constraints) - 1 - len(lsubset)) - ) / math.factorial(len(constraints)) - sub_ctrb = weight * ( - self.determine_conformance_rate(log, constraints_without) - - self.determine_conformance_rate(log, constraints_with) - ) - sub_ctrbs.append(sub_ctrb) - return sum(sub_ctrbs) - - def evaluate_similarity(self, trace): - """ - Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance. - - :param trace: The trace to compare with the adherent trace. - :return: A normalized score indicating the similarity between the adherent trace and the given trace. - """ - length = 0 - trace_len = len("".join(trace)) - lev_distance = 0 - if self.minimal_solution: - lev_distance = ( - len(max(self.adherent_traces)) + trace_len - ) # The maximum possible levenshtein distance - length = len(max(self.adherent_traces)) - for t in self.adherent_traces: - tmp_dist = levenshtein_distance(t, "".join(trace)) - if lev_distance > tmp_dist: # Closer to a possible solution - lev_distance = tmp_dist - else: - length = len(self.adherent_trace) - lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) - max_distance = max(length, trace_len) - normalized_score = 1 - lev_distance / max_distance - return normalized_score - - def determine_conformance_rate(self, event_log, constraints=None): - """ - Determines the conformance rate of the event log based on the given constraints. - - :param event_log: The event log to analyze. - :param constraints: The constraints to check against the event log. - :return: The conformance rate as a float between 0 and 1, or a message if no constraints are provided. - """ - if not self.constraints and not constraints: - return "The explainer have no constraints" - len_log = len(event_log) - if len_log == 0: - return 1 - non_conformant = 0 - if constraints == None: - constraints = self.constraints - for trace, count in event_log.log.items(): - for con in constraints: - if not re.search(con, "".join(trace)): - non_conformant += count - break - return (len_log - non_conformant) / len_log - - def trace_contribution_to_conformance_loss( - self, event_log, trace, constraints=None - ): - """ - Calculates the contribution of a specific trace to the conformance loss of the event log. - - :param event_log: The event log to analyze. - :param trace: The trace to calculate its contribution. - :param constraints: The constraints to check against the event log. - :return: The contribution of the trace to the conformance loss as a float between 0 and 1. - """ - if not constraints: - constraints = self.constraints - total_traces = len(event_log) - contribution_of_trace = 0 - for t, count in event_log.log.items(): - if not self.conformant(t, constraints): - if trace.nodes == list(t): - contribution_of_trace = count - - return contribution_of_trace / total_traces - - -def determine_powerset(elements): - """Determines the powerset of a list of elements - Args: - elements (set): Set of elements - Returns: - list: Powerset of elements - """ - lset = list(elements) - ps_elements = chain.from_iterable( - combinations(lset, option) for option in range(len(lset) + 1) - ) - return [set(ps_element) for ps_element in ps_elements] - - -def get_sublists(lst): - """ - Generates all possible non-empty sublists of a list. - - :param lst: The input list. - :return: A list of all non-empty sublists. - """ - sublists = [] - for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n - sublists.extend(combinations(lst, r)) - return sublists - - -def get_iterative_subtrace(trace): - """ - Generates all possible non-empty contiguous sublists of a list, maintaining order. - - :param lst: The input list. - n: the minmum length of sublists - :return: A list of all non-empty contiguous sublists. - """ - sublists = [] - for i in range(0, len(trace)): - sublists.append(trace.nodes[0 : i + 1]) - - return sublists - - -def levenshtein_distance(seq1, seq2): - """ - Calculates the Levenshtein distance between two sequences. - - Args: - seq1 (str): The first sequence. - seq2 (str): The second sequence. - - Returns: - int: The Levenshtein distance between the two sequences. - """ - size_x = len(seq1) + 1 - size_y = len(seq2) + 1 - matrix = [[0] * size_y for _ in range(size_x)] - for x in range(size_x): - matrix[x][0] = x - for y in range(size_y): - matrix[0][y] = y - - for x in range(1, size_x): - for y in range(1, size_y): - if seq1[x - 1] == seq2[y - 1]: - matrix[x][y] = matrix[x - 1][y - 1] - else: - matrix[x][y] = min( - matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 - ) - return matrix[size_x - 1][size_y - 1] diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py new file mode 100644 index 0000000..0e72857 --- /dev/null +++ b/explainer/explainer_regex.py @@ -0,0 +1,511 @@ +import math +import re +from itertools import combinations, product, chain +from explainer import Explainer, Trace, EventLog + +class ExplainerRegex(Explainer): + def __init__(self): + super().__init__() + + def set_minimal_solution(self, minimal_solution): + """ + Tells the explainer to generate minimal solutions + Note: This will increase computations significantly + + Args: + minimal_solution (bool): True to generate minimal solutions, False if it should be the first possible + """ + self.minimal_solution = minimal_solution + + def add_constraint(self, regex): + """ + Adds a new constraint and updates the nodes list. + + :param regex: A regular expression representing the constraint. + """ + self.constraints.append(regex) + if self.contradiction(): + self.constraints.remove(regex) + print(f"Constraint {regex} contradicts the other constraints.") + + def remove_constraint(self, idx): + """ + Removes a constraint by index and updates the nodes list if necessary. + + :param idx: Index of the constraint to be removed. + """ + if 0 <= idx < len(self.constraints): + removed_regex = self.constraints.pop(idx) + removed_nodes = set(filter(str.isalpha, removed_regex)) + + # Re-evaluate nodes to keep based on remaining constraints + remaining_nodes = set(filter(str.isalpha, "".join(self.constraints))) + self.nodes = remaining_nodes + + # Optionally, remove nodes that are no longer in any constraint + for node in removed_nodes: + if node not in remaining_nodes: + self.nodes.discard(node) + + def activation(self, trace, constraints=None): + """ + Checks if any of the nodes in the trace activates any constraint. + + :param trace: A Trace instance. + :return: Boolean indicating if any constraint is activated. + """ + if not constraints: + constraints = self.constraints + con_activation = [0] * len(constraints) + activated = False + for idx, con in enumerate(constraints): + if activated: + activated = False + continue + target = self.identify_existance_constraints(con) + if target: + con_activation[idx] = 1 + continue + for event in trace: + if event in con: + con_activation[idx] = 1 + activated = True + break + return con_activation + + def identify_existance_constraints(self, pattern): + """ + Identifies existance constraints within a pattern. + + :param pattern: The constraint pattern as a string. + :return: A tuple indicating the type of existance constraint and the node involved. + """ + # Check for AtLeastOne constraint + for match in re.finditer(r"(? 100: + return f"{explanation}\n Maximum depth of {depth -1} reached" + score = self.evaluate_similarity(working_trace) + return self.operate_on_trace(working_trace, score, explanation, depth) + + def operate_on_trace(self, trace, score, explanation_path, depth=0): + """ + Finds and applies modifications to the trace to make it conformant. + + :param trace: The trace to be modified. + :param score: The similarity score of the trace. + :param explanation_path: The current explanation path. + :param depth: The current recursion depth. + :return: A string explaining why the best subtrace is non-conformant or a message indicating the maximum depth has been reached. + """ + explanation = None + counter_factuals = self.modify_subtrace(trace) + best_subtrace = None + best_score = -float("inf") + for subtrace in counter_factuals: + current_score = self.evaluate_similarity(subtrace[0]) + if current_score > best_score and current_score > score: + best_score = current_score + best_subtrace = subtrace[0] + explanation = subtrace[1] + if best_subtrace == None: + for subtrace in counter_factuals: + self.operate_on_trace(subtrace[0], score, explanation_path, depth + 1) + explanation_string = explanation_path + "\n" + str(explanation) + return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) + + def get_nodes_from_constraint(self, constraint=None): + """ + Extracts unique nodes from a constraint pattern. + + :param constraint: The constraint pattern as a string. + :return: A list of unique nodes found within the constraint. + """ + if constraint is None: + all_nodes = set() + for con in self.constraints: + all_nodes.update(re.findall(r"[A-Za-z]", con)) + return list(set(all_nodes)) + else: + return list(set(re.findall(r"[A-Za-z]", constraint))) + + def modify_subtrace(self, trace): + """ + Modifies the given trace to meet constraints by adding nodes where the pattern fails. + + Parameters: + - trace: A list of node identifiers + + Returns: + - A list of potential subtraces each modified to meet constraints. + """ + potential_subtraces = [] + possible_additions = self.get_nodes_from_constraint() + for i, s_trace in enumerate(get_iterative_subtrace(trace)): + for con in self.constraints: + new_trace_str = "".join(s_trace) + match = re.match(new_trace_str, con) + if not match: + for add in possible_additions: + potential_subtraces.append( + [ + Trace(s_trace + [add] + trace.nodes[i + 1 :]), + f"Addition (Added {add} at position {i+1}): " + + "->".join(s_trace + [add] + trace.nodes[i + 1 :]), + ] + ) + potential_subtraces.append( + [ + Trace(s_trace[:-1] + [add] + trace.nodes[i:]), + f"Addition (Added {add} at position {i}): " + + "->".join(s_trace[:-1] + [add] + trace.nodes[i:]), + ] + ) + + potential_subtraces.append( + [ + Trace(s_trace[:-1] + trace.nodes[i + 1 :]), + f"Subtraction (Removed {s_trace[i]} from position {i}): " + + "->".join(s_trace[:-1] + trace.nodes[i + 1 :]), + ] + ) + return potential_subtraces + + def evaluate_similarity(self, trace): + """ + Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance. + + :param trace: The trace to compare with the adherent trace. + :return: A normalized score indicating the similarity between the adherent trace and the given trace. + """ + length = 0 + trace_len = len("".join(trace)) + lev_distance = 0 + if self.minimal_solution: + lev_distance = ( + len(max(self.adherent_traces)) + trace_len + ) # The maximum possible levenshtein distance + length = len(max(self.adherent_traces)) + for t in self.adherent_traces: + tmp_dist = levenshtein_distance(t, "".join(trace)) + if lev_distance > tmp_dist: # Closer to a possible solution + lev_distance = tmp_dist + else: + length = len(self.adherent_trace) + lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) + max_distance = max(length, trace_len) + normalized_score = 1 - lev_distance / max_distance + return normalized_score + + def determine_conformance_rate(self, event_log, constraints=None): + """ + Determines the conformance rate of the event log based on the given constraints. + + :param event_log: The event log to analyze. + :param constraints: The constraints to check against the event log. + :return: The conformance rate as a float between 0 and 1, or a message if no constraints are provided. + """ + if not self.constraints and not constraints: + return "The explainer have no constraints" + len_log = len(event_log) + if len_log == 0: + return 1 + non_conformant = 0 + if constraints == None: + constraints = self.constraints + for trace, count in event_log.log.items(): + for con in constraints: + if not re.search(con, "".join(trace)): + non_conformant += count + break + return (len_log - non_conformant) / len_log + + def determine_fitness_rate(self, event_log, constraints = None): + if not self.constraints and not constraints: + return "The explainer have no constraints" + if constraints == None: + constraints = self.constraints + conformant = 0 + for con in constraints: + for trace, count in event_log.log.items(): + if re.search(con, "".join(trace)): + conformant += count + return conformant / (len(event_log) * len(constraints)) + + def variant_ctrb_to_fitness( + self, event_log, trace, constraints=None + ): + if not self.constraints and not constraints: + return "The explainer have no constraints" + if not constraints: + constraints = self.constraints + total_traces = len(event_log) + contribution_of_trace = 0 + for con in constraints: + if re.search(con, "".join(trace)): + contribution_of_trace += 1 + nr = event_log.get_variant_count(trace) + contribution_of_trace = contribution_of_trace / len(constraints) + contribution_of_trace = nr * contribution_of_trace + return contribution_of_trace / total_traces + + + def variant_ctrb_to_conformance_loss( + self, event_log, trace, constraints=None + ): + """ + Calculates the contribution of a specific trace to the conformance loss of the event log. + + :param event_log: The event log to analyze. + :param trace: The trace to calculate its contribution. + :param constraints: The constraints to check against the event log. + :return: The contribution of the trace to the conformance loss as a float between 0 and 1. + """ + if not self.constraints and not constraints: + return "The explainer have no constraints" + if not constraints: + constraints = self.constraints + total_traces = len(event_log) + contribution_of_trace = 0 + + if not self.conformant(trace, constraints= constraints): + contribution_of_trace = event_log.get_variant_count(trace) + + return contribution_of_trace / total_traces + + def constraint_ctrb_to_conformance(self, log, constraints, index): + """Determines the Shapley value-based contribution of a constraint to a the + overall conformance rate. + Args: + log (dictionary): The event log, where keys are strings and values are + ints + constraints (list): A list of constraints (regexp strings) + index (int): The + Returns: + float: The contribution of the constraint to the overall conformance + rate + """ + if len(constraints) < index: + raise Exception("Constraint not in constraint list.") + contributor = constraints[index] + sub_ctrbs = [] + reduced_constraints = [c for c in constraints if not c == contributor] + subsets = determine_powerset(reduced_constraints) + for subset in subsets: + lsubset = list(subset) + constraints_without = [c for c in constraints if c in lsubset] + constraints_with = [c for c in constraints if c in lsubset + [contributor]] + weight = ( + math.factorial(len(lsubset)) + * math.factorial(len(constraints) - 1 - len(lsubset)) + ) / math.factorial(len(constraints)) + sub_ctrb = weight * ( + self.determine_conformance_rate(log, constraints_without) + - self.determine_conformance_rate(log, constraints_with) + ) + sub_ctrbs.append(sub_ctrb) + return sum(sub_ctrbs) + + def constraint_ctrb_to_fitness(self, log, constraints, index): + if len(constraints) < index: + raise Exception("Constraint not in constraint list.") + if not self.constraints and not constraints: + return "The explainer have no constraints" + if not constraints: + constraints = self.constraints + contributor = constraints[index] + ctrb_count = 0 + for trace, count in log.log.items(): + if re.search(contributor, "".join(trace)): + ctrb_count += count + return ctrb_count / (len(log) * len(constraints)) + +def determine_powerset(elements): + """Determines the powerset of a list of elements + Args: + elements (set): Set of elements + Returns: + list: Powerset of elements + """ + lset = list(elements) + ps_elements = chain.from_iterable( + combinations(lset, option) for option in range(len(lset) + 1) + ) + return [set(ps_element) for ps_element in ps_elements] + + +def get_sublists(lst): + """ + Generates all possible non-empty sublists of a list. + + :param lst: The input list. + :return: A list of all non-empty sublists. + """ + sublists = [] + for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n + sublists.extend(combinations(lst, r)) + return sublists + + +def get_iterative_subtrace(trace): + """ + Generates all possible non-empty contiguous sublists of a list, maintaining order. + + :param lst: The input list. + n: the minmum length of sublists + :return: A list of all non-empty contiguous sublists. + """ + sublists = [] + for i in range(0, len(trace)): + sublists.append(trace.nodes[0 : i + 1]) + + return sublists + + +def levenshtein_distance(seq1, seq2): + """ + Calculates the Levenshtein distance between two sequences. + + Args: + seq1 (str): The first sequence. + seq2 (str): The second sequence. + + Returns: + int: The Levenshtein distance between the two sequences. + """ + size_x = len(seq1) + 1 + size_y = len(seq2) + 1 + matrix = [[0] * size_y for _ in range(size_x)] + for x in range(size_x): + matrix[x][0] = x + for y in range(size_y): + matrix[0][y] = y + + for x in range(1, size_x): + for y in range(1, size_y): + if seq1[x - 1] == seq2[y - 1]: + matrix[x][y] = matrix[x - 1][y - 1] + else: + matrix[x][y] = min( + matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 + ) + return matrix[size_x - 1][size_y - 1] diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py new file mode 100644 index 0000000..8233b08 --- /dev/null +++ b/explainer/explainer_signal.py @@ -0,0 +1,69 @@ +from abc import ABC, abstractmethod +from explainer import Explainer, Trace, EventLog +import re +from tutorial import SignavioAuthenticator +import json +from conf import system_instance, workspace_id, user_name, pw +class ExplainerSignal(Explainer): + def __init__(self): + super().__init__() + authenticator = SignavioAuthenticator(system_instance, workspace_id, user_name, pw) + auth_data = authenticator.authenticate() + cookies = {'JSESSIONID': auth_data['jsesssion_ID'], 'LBROUTEID': auth_data['lb_route_ID']} + headers = {'Accept': 'application/json', 'x-signavio-id': auth_data['auth_token']} + #diagram_url = system_instance + '/p/revision' + def remove_constraint(self, idx): + """ + Removes a constraint by index and updates the nodes list if necessary. + + :param idx: Index of the constraint to be removed. + """ + if idx < len(self.constraints): + del self.constraints[idx] + + def activation(self, trace, constraints=None): + """ + Checks if any of the nodes in the trace activates any constraint. + + :param trace: A Trace instance. + :return: Boolean indicating if any constraint is activated. + """ + if constraints is None: + constraints = self.constraints + + for node in trace: + for constraint in constraints: + if re.search(constraint, node): + return True + return False + + def conformant(self, trace, constraints=None): + if not constraints: + constraints = self.constraints + + pass + def contradiction(self, check_multiple=False, max_length=10): + pass + + def minimal_expl(self, trace): + pass + + def counterfactual_expl(self, trace): + pass + + def determine_shapley_value(self, log, constraints, index): + pass + + def evaluate_similarity(self, trace): + pass + + def determine_conformance_rate(self, event_log, constraints=None): + pass + + def trace_contribution_to_conformance_loss(self, event_log, trace, constraints=None): + pass + + +exp = ExplainerSignal() +exp.add_constraint("(^'A')") +print(exp.constraints) \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index 8dfb5f3..bf1dbac 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -26,7 +26,8 @@ "source": [ "import sys\n", "sys.path.append('../')\n", - "from explainer import Explainer, Trace" + "from explainer import Explainer, Trace, EventLog\n", + "from explainer_regex import ExplainerRegex\n" ] }, { @@ -43,7 +44,7 @@ "metadata": {}, "outputs": [], "source": [ - "explainer = Explainer()\n", + "explainer = ExplainerRegex()\n", "explainer.add_constraint('A.*B.*C')" ] }, @@ -203,42 +204,22 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 5, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Example without minimal solution\n", - "--------------------------------\n", - "\n", - "Subtraction (Removed A from position 2): A->B->C->B\n", - "Subtraction (Removed B from position 3): A->B->C\n", - "\n", - "Example with minimal solution\n", - "--------------------------------\n", - "\n", - "Addition (Added C at position 5): A->B->A->C->B->C\n", - "\n", - "Example without minimal solution\n", - "--------------------------------\n", - "\n", - "Addition (Added A at position 1): C->A->B->A\n", - "Subtraction (Removed C from position 0): A->B->A\n", - "Addition (Added C at position 2): A->B->C->A\n", - "Subtraction (Removed A from position 3): A->B->C\n", - "\n", - "Example with minimal solution\n", - "--------------------------------\n", - "\n", - "Addition (Added A at position 0): A->C->B->A\n", - "Addition (Added C at position 4): A->C->B->A->C\n" - ] + "data": { + "text/plain": [ + "'exp = ExplainerRegex()\\nexp.add_constraint(\"^A\")\\nexp.add_constraint(\"A.*B.*\")\\nexp.add_constraint(\"C$\")\\ntrace = Trace([\\'A\\', \\'B\\',\\'A\\',\\'C\\', \\'B\\'])\\nprint(\"Example without minimal solution\")\\nprint(\"--------------------------------\")\\nprint(exp.counterfactual_expl(trace))\\n\\nprint(\"\\nExample with minimal solution\")\\nprint(\"--------------------------------\")\\nexp.set_minimal_solution(True)\\nprint(exp.counterfactual_expl(trace))\\nexp.set_minimal_solution(False)\\ntrace = Trace([\\'C\\',\\'B\\',\\'A\\'])\\nprint(\"\\nExample without minimal solution\")\\nprint(\"--------------------------------\")\\nprint(exp.counterfactual_expl(trace))\\n\\nprint(\"\\nExample with minimal solution\")\\nprint(\"--------------------------------\")\\nexp.set_minimal_solution(True)\\nprint(exp.counterfactual_expl(trace))'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "exp = Explainer()\n", + "\"\"\"exp = ExplainerRegex()\n", "exp.add_constraint(\"^A\")\n", "exp.add_constraint(\"A.*B.*\")\n", "exp.add_constraint(\"C$\")\n", @@ -260,16 +241,22 @@ "print(\"\\nExample with minimal solution\")\n", "print(\"--------------------------------\")\n", "exp.set_minimal_solution(True)\n", - "print(exp.counterfactual_expl(trace))" + "print(exp.counterfactual_expl(trace))\"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 6: Event Logs and Shapely values\n", + "## Step 6: Contribution functions and Event Logs\n", "\n", - "The event logs in this context is built with traces, here's how you set them up." + "For this project, 4 contribution functions have been developed to determined a trace variant's, or constraint's contribution to a system.\n", + "\n", + "For the sake efficiency, all of the contribution functions, `variant_ctrb_to_conformance_loss`, `variant_ctrb_to_fitness`,`constraint_ctrb_to_fitness` and `constraint_ctrb_to_conformance`, should equal the total amount of conformance loss and fitness rate.\n", + "\n", + "There are to methods to determine the conformance rate (and conformance loss, by extension) and the fitness rate; `determine_conformance_rate` and `determine_fitness_rate`. \n", + "\n", + "All of these methods utilized an abstraction of an Event Log. In this block, the initialization and usage of conformance rate and fitness rate is displayed." ] }, { @@ -281,54 +268,194 @@ "name": "stdout", "output_type": "stream", "text": [ - "Conformance rate: 0.2\n", - "Contribution ^A: 0.5\n", - "Contribution C$: 0.30000000000000004\n" + "Conformance rate: 20.0%\n", + "Fitness rate: 50.0%\n" ] } ], "source": [ - "from explainer import EventLog\n", - "\n", + "exp = ExplainerRegex()\n", + "# Setup an event log\n", "event_log = EventLog()\n", - "trace1 = Trace(['A', 'B', 'C'])\n", - "trace2 = Trace(['B', 'C'])\n", - "trace3 = Trace(['A', 'B'])\n", - "trace4 = Trace(['B'])\n", + "traces = [\n", + " Trace(['A', 'B','C']),\n", + " Trace(['A', 'B']),\n", + " Trace(['B']),\n", + " Trace(['B','C'])\n", + "]\n", + "event_log.add_trace(traces[0], 10) # The second parameter is how many variants you'd like to add, leave blank for 1\n", + "event_log.add_trace(traces[1], 10)\n", + "event_log.add_trace(traces[2], 10)\n", + "event_log.add_trace(traces[3], 20)\n", + "# Add the constraints\n", + "exp.add_constraint('^A')\n", + "exp.add_constraint('C$')\n", "\n", - "event_log.add_trace(trace1, 5) # The second is how many traces you'd like to add, leave blank for 1\n", - "event_log.add_trace(trace2, 10)\n", - "event_log.add_trace(trace3, 5)\n", - "event_log.add_trace(trace4, 5)\n", + "print(\"Conformance rate: \" + str(exp.determine_conformance_rate(event_log) * 100) + \"%\")\n", + "print(\"Fitness rate: \" + str(exp.determine_fitness_rate(event_log) * 100) + \"%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_conformance_loss` determines how much a specific variant contributes to the overall conformance loss" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution of variant to conformance rate\n", + "Ctrb of variant ['A', 'B', 'C']: 0.0\n", + "Ctrb of variant ['A', 'B']: 0.2\n", + "Ctrb of variant ['B']: 0.2\n", + "Ctrb of variant ['B', 'C']: 0.4\n", + "Total conformance loss: 0.8\n" + ] + } + ], + "source": [ + "print(\"Contribution of variant to conformance rate\")\n", + "print(\"Ctrb of variant \"+ str(traces[0].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[0])))\n", + "print(\"Ctrb of variant \"+ str(traces[1].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[1])))\n", + "print(\"Ctrb of variant \"+ str(traces[2].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[2])))\n", + "print(\"Ctrb of variant \"+ str(traces[3].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[3])))\n", + "print(\"Total conformance loss: \" + str(exp.variant_ctrb_to_conformance_loss(event_log, traces[0]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[1]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[2]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[3])))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_fitness` determines how much a specific variant contributes to the overall fitness rate" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution of variant to fitness rate\n", + "Ctrb of variant ['A', 'B', 'C']: 0.2\n", + "Ctrb of variant ['A', 'B']: 0.1\n", + "Ctrb of variant ['B']: 0.0\n", + "Ctrb of variant ['B', 'C']: 0.2\n", + "Total fitness: 0.5\n" + ] + } + ], + "source": [ + "print(\"Contribution of variant to fitness rate\")\n", + "print(\"Ctrb of variant \" + str(traces[0].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[0]), 2)))\n", + "print(\"Ctrb of variant \" + str(traces[1].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[1]), 2)))\n", + "print(\"Ctrb of variant \" + str(traces[2].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[2]), 2)))\n", + "print(\"Ctrb of variant \" + str(traces[3].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[3]), 2)))\n", + "total_fitness = (exp.variant_ctrb_to_fitness(event_log, traces[0]) +\n", + " exp.variant_ctrb_to_fitness(event_log, traces[1]) +\n", + " exp.variant_ctrb_to_fitness(event_log, traces[2]) +\n", + " exp.variant_ctrb_to_fitness(event_log, traces[3]))\n", + "print(\"Total fitness: \" + str(round(total_fitness, 2)))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`constraint_ctrb_to_fitness` determines how much a specific constraint contributes to the overall fitness rate" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "^A ctrb to fitness rate: 0.2\n", + "B$ ctrb to fitness rate: 0.3\n", + "Total fitness: 0.5\n" + ] + } + ], + "source": [ + "\n", + "print(\"^A ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0)))\n", + "print(\"B$ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))\n", + "print(\"Total fitness: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 7: Shapely values\n", "\n", + "`constraint_ctrb_to_conformance` determines how much a specific constraint contributes to the overall conformance loss. \n", "\n", - "exp = Explainer()\n", - "exp.add_constraint(\"^A\")\n", - "exp.add_constraint(\"C$\")\n", - "print(\"Conformance rate: \"+ str(exp.determine_conformance_rate(event_log)))\n", - "print('Contribution ^A:', exp.determine_shapley_value(event_log, exp.constraints, 0))\n", - "print('Contribution C$:', exp.determine_shapley_value(event_log, exp.constraints, 1))\n" + "Because the constraints overlap in this case, Shapley values have been used to determine the contribution. This makes the method more complicated and more computationally heavy than the other contribution functions \n" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contriution of constraint to conformance rate\n", + "^A ctrb: 0.5\n", + "C$ ctrb: 0.30000000000000004 (adjusted 0.3)\n", + "Total conformance loss: 0.8\n" + ] + } + ], + "source": [ + "print(\"Contriution of constraint to conformance rate\")\n", + "print(\"^A ctrb: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0)))\n", + "print(\"C$ ctrb: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1)) + \" (adjusted \" + str(round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1), 2)) + \")\")\n", + "print(\"Total conformance loss: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1)))\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "conformant AC :True\n", "Conformance rate: 0.14\n", - "Contribution C$: 0.21\n", - "Contribution ^A: 0.36\n", - "Contribution B+: 0.29\n" + "Contribution C$: 0.21\n", + "Contribution ^A: 0.36\n", + "Contribution B+: 0.29\n", + "Conformance loss = 86.0%, contribution to loss: 86.0%\n", + "------------------------------------\n", + "Fitness rate: 0.6666666666666666\n", + "C$ ctrb to fitness rate: 0.23809523809523808\n", + "^A ctrb to fitness rate: 0.19047619047619047\n", + "B+ ctrb to fitness rate: 0.23809523809523808\n", + "Total fitness: 0.6666666666666666\n" ] } ], "source": [ - "exp = Explainer()\n", + "exp = ExplainerRegex()\n", "event_log = EventLog()\n", "trace1 = Trace(['A', 'B', 'C'])\n", "trace2 = Trace(['B', 'C'])\n", @@ -337,23 +464,33 @@ "trace5 = Trace(['A', 'C'])\n", "\n", "\n", - "event_log.add_trace(trace1, 5) # The second is how many traces you'd like to add, leave blank for 1\n", + "event_log.add_trace(trace1, 5) \n", "event_log.add_trace(trace2, 10)\n", "event_log.add_trace(trace3, 5)\n", "event_log.add_trace(trace4, 5)\n", "event_log.add_trace(trace5, 10)\n", "\n", "\n", - "exp = Explainer()\n", + "exp = ExplainerRegex()\n", "exp.add_constraint(\"C$\")\n", "exp.add_constraint(\"^A\")\n", "exp.add_constraint(\"B+\")\n", - "print(\"conformant AC :\" + str(exp.conformant(trace5)))\n", - "print(\"Conformance rate: \"+ str(round(exp.determine_conformance_rate(event_log), 2)))\n", - "print('Contribution C$:', round(exp.determine_shapley_value(event_log, exp.constraints, 0), 2))\n", - "print('Contribution ^A:', round(exp.determine_shapley_value(event_log, exp.constraints, 1), 2))\n", - "print('Contribution B+:', round(exp.determine_shapley_value(event_log, exp.constraints, 2), 2))\n", - "\n" + "conf_rate = exp.determine_conformance_rate(event_log)\n", + "print(\"Conformance rate: \"+ str(round(conf_rate, 2)))\n", + "print(\"Contribution C$: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0), 2)) # Round for easier readability\n", + "print(\"Contribution ^A: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1), 2))\n", + "print(\"Contribution B+: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 2), 2))\n", + "total_ctrb = exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 2)\n", + "conf_rate = round(conf_rate, 2) \n", + "total_ctrb = round(total_ctrb, 2)\n", + "print(\"Conformance loss = \" + str(100 - (conf_rate * 100)) + \"%, contribution to loss: \" + str(total_ctrb * 100) + \"%\")\n", + "print(\"------------------------------------\")\n", + "print(\"Fitness rate: \"+ str(exp.determine_fitness_rate(event_log)))\n", + "print(\"C$ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0)))\n", + "print(\"^A ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))\n", + "print(\"B+ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 2)))\n", + "\n", + "print(\"Total fitness: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 2)))\n" ] } ], diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index e22fb32..7a022e4 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -1,16 +1,16 @@ from explainer.explainer import * - +from explainer.explainer_regex import ExplainerRegex # Test 1: Adding and checking constraints def test_add_constraint(): - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") assert "A.*B.*C" in explainer.constraints, "Constraint 'A.*B.*C' should be added." # Test 2: Removing constraints def test_remove_constraint(): - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") explainer.add_constraint("B.*C") explainer.remove_constraint(0) @@ -22,7 +22,7 @@ def test_remove_constraint(): # Test 3: Activation of constraints def test_activation(): trace = Trace(["A", "B", "C"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") assert explainer.activation(trace), "The trace should activate the constraint." @@ -30,7 +30,7 @@ def test_activation(): # Test 4: Checking conformance of traces def test_conformance(): trace = Trace(["A", "B", "C"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") assert explainer.conformant(trace), "The trace should be conformant." @@ -38,7 +38,7 @@ def test_conformance(): # Test 5: Non-conformance explanation def test_non_conformance_explanation(): trace = Trace(["C", "A", "B"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") explanation = explainer.minimal_expl(trace) assert "violated" in explanation, "The explanation should indicate a violation." @@ -47,7 +47,7 @@ def test_non_conformance_explanation(): # Test 6: Overlapping constraints def test_overlapping_constraints(): trace = Trace(["A", "B", "A", "C"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") explainer.add_constraint("A.*A.*C") assert explainer.conformant( @@ -58,7 +58,7 @@ def test_overlapping_constraints(): # Test 7: Partially meeting constraints def test_partial_conformance(): trace = Trace(["A", "C", "B"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") assert not explainer.conformant(trace), "The trace should not be fully conformant." @@ -66,7 +66,7 @@ def test_partial_conformance(): # Test 8: Constraints with repeated nodes def test_constraints_with_repeated_nodes(): trace = Trace(["A", "A", "B", "A"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*A.*B.*A") assert explainer.conformant( trace @@ -75,7 +75,7 @@ def test_constraints_with_repeated_nodes(): # Test 9: Removing constraints and checking nodes list def test_remove_constraint_and_check_nodes(): - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B") explainer.add_constraint("B.*C") explainer.remove_constraint(0) @@ -87,7 +87,7 @@ def test_remove_constraint_and_check_nodes(): # Test 10: Complex regex constraint def test_complex_regex_constraint(): trace = Trace(["A", "X", "B", "Y", "C"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint( "A.*X.*B.*Y.*C" ) # Specifically expects certain nodes in order @@ -99,7 +99,7 @@ def test_complex_regex_constraint(): # Test 11: Constraint not covered by any trace node def test_constraint_not_covered(): trace = Trace(["A", "B", "C"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("D*") # This node "D" does not exist in the trace assert explainer.activation(trace) == [ 0 @@ -109,7 +109,7 @@ def test_constraint_not_covered(): # Test 12: Empty trace and constraints def test_empty_trace_and_constraints(): trace = Trace([]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("") # Adding an empty constraint assert explainer.conformant( trace @@ -118,7 +118,7 @@ def test_empty_trace_and_constraints(): # Test 13: Removing non-existent constraint index def test_remove_nonexistent_constraint(): - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B") explainer.remove_constraint(10) # Non-existent index assert ( @@ -129,7 +129,7 @@ def test_remove_nonexistent_constraint(): # Test 14: Activation with no constraints def test_activation_with_no_constraints(): trace = Trace(["A", "B", "C"]) - explainer = Explainer() + explainer = ExplainerRegex() assert not explainer.activation(trace), "No constraints should mean no activation." @@ -142,7 +142,7 @@ def test_trace_conformance_against_multiple_constraints(): ["A", "B", "C", "D"] ) # This trace should be conformant as it matches both constraints - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") # Both traces attempt to conform to this explainer.add_constraint("B.*D") # And to this @@ -158,7 +158,7 @@ def test_trace_conformance_against_multiple_constraints(): # Test 16: Conformant trace does not generate minimal explaination def test_conformant_trace_handled_correctly(): trace = Trace(["A", "B"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("AB") assert ( @@ -170,7 +170,7 @@ def test_conformant_trace_handled_correctly(): # Test 17: Conformant trace def test_explainer_methods(): trace = Trace(["A", "B", "C"]) - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("A.*B.*C") explainer.add_constraint("B.*C") @@ -189,7 +189,7 @@ def test_explainer_methods(): # Test 18: Some explaination test def test_explaination(): - explainer = Explainer() + explainer = ExplainerRegex() conformant_trace = Trace(["A", "B", "C"]) non_conformant_trace = Trace(["A", "C"]) @@ -216,7 +216,7 @@ def test_explaination(): def test_complex_counterfactual_explanation(): - explainer = Explainer() + explainer = ExplainerRegex() explainer.add_constraint("ABB*C") From c1a8211c0d065de872675dedb94d344771c8cc2f Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 23 Apr 2024 14:37:17 +0200 Subject: [PATCH 29/54] trail commit --- explainer/tutorial/explainer_tutorial.ipynb | 62 ++++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index bf1dbac..d2b2630 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -59,7 +59,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -87,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -204,22 +204,42 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 27, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'exp = ExplainerRegex()\\nexp.add_constraint(\"^A\")\\nexp.add_constraint(\"A.*B.*\")\\nexp.add_constraint(\"C$\")\\ntrace = Trace([\\'A\\', \\'B\\',\\'A\\',\\'C\\', \\'B\\'])\\nprint(\"Example without minimal solution\")\\nprint(\"--------------------------------\")\\nprint(exp.counterfactual_expl(trace))\\n\\nprint(\"\\nExample with minimal solution\")\\nprint(\"--------------------------------\")\\nexp.set_minimal_solution(True)\\nprint(exp.counterfactual_expl(trace))\\nexp.set_minimal_solution(False)\\ntrace = Trace([\\'C\\',\\'B\\',\\'A\\'])\\nprint(\"\\nExample without minimal solution\")\\nprint(\"--------------------------------\")\\nprint(exp.counterfactual_expl(trace))\\n\\nprint(\"\\nExample with minimal solution\")\\nprint(\"--------------------------------\")\\nexp.set_minimal_solution(True)\\nprint(exp.counterfactual_expl(trace))'" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Example without minimal solution\n", + "--------------------------------\n", + "\n", + "Subtraction (Removed A from position 2): A->B->C->B\n", + "Subtraction (Removed B from position 3): A->B->C\n", + "\n", + "Example with minimal solution\n", + "--------------------------------\n", + "\n", + "Addition (Added C at position 5): A->B->A->C->B->C\n", + "\n", + "Example without minimal solution\n", + "--------------------------------\n", + "\n", + "Addition (Added A at position 1): C->A->B->A\n", + "Subtraction (Removed C from position 0): A->B->A\n", + "Addition (Added C at position 2): A->B->C->A\n", + "Subtraction (Removed A from position 3): A->B->C\n", + "\n", + "Example with minimal solution\n", + "--------------------------------\n", + "\n", + "Addition (Added A at position 0): A->C->B->A\n", + "Addition (Added C at position 4): A->C->B->A->C\n" + ] } ], "source": [ - "\"\"\"exp = ExplainerRegex()\n", + "exp = ExplainerRegex()\n", "exp.add_constraint(\"^A\")\n", "exp.add_constraint(\"A.*B.*\")\n", "exp.add_constraint(\"C$\")\n", @@ -241,7 +261,7 @@ "print(\"\\nExample with minimal solution\")\n", "print(\"--------------------------------\")\n", "exp.set_minimal_solution(True)\n", - "print(exp.counterfactual_expl(trace))\"\"\"" + "print(exp.counterfactual_expl(trace))" ] }, { @@ -261,7 +281,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -304,7 +324,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 29, "metadata": {}, "outputs": [ { @@ -338,7 +358,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 30, "metadata": {}, "outputs": [ { @@ -376,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -409,7 +429,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -433,7 +453,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 33, "metadata": {}, "outputs": [ { From 9b04b6a33f04b8894e25c66fb7efd884260c1c07 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 23 Apr 2024 14:50:24 +0200 Subject: [PATCH 30/54] Optimized the algorithm for counterfactual --- explainer/explainer_regex.py | 58 ++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 0e72857..e830874 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -24,7 +24,10 @@ def add_constraint(self, regex): :param regex: A regular expression representing the constraint. """ self.constraints.append(regex) - if self.contradiction(): + max_length = 0 + for con in self.constraints: + max_length += len(con) + if self.contradiction(len(regex) + max_length): self.constraints.remove(regex) print(f"Constraint {regex} contradicts the other constraints.") @@ -113,7 +116,7 @@ def conformant(self, trace, constraints=None): return all(re.search(constraint, trace_str) for constraint in constraints) return all(re.search(constraint, trace_str) for constraint in self.constraints) - def contradiction(self, check_multiple=False, max_length=10): + def contradiction(self, max_length): """ Checks if there is a contradiction among the constraints. @@ -125,12 +128,28 @@ def contradiction(self, check_multiple=False, max_length=10): for combination in product(nodes, repeat=length): test_str = "".join(combination) if all(re.search(con, test_str) for con in self.constraints): - if not check_multiple: # Standard, if solution is not minimal self.adherent_trace = test_str return False # Found a match - else: - self.adherent_traces.append(test_str) - return not check_multiple # No combination satisfied all constraints + return True # No combination satisfied all constraints + + def contradiction_by_length(self, length): + """ + Checks if there is a contradiction among the constraints specifically for a given length. + + :param length: The specific length of combinations to test. + :return: Boolean indicating if there is a contradiction. + """ + nodes = self.get_nodes_from_constraint() + nodes = nodes + nodes # Assuming you need to double the nodes as in your previous snippet + + for combination in product(nodes, repeat=length): + test_str = "".join(combination) + if all(re.search(con, test_str) for con in self.constraints): + self.adherent_trace = test_str + return False # Found a match that satisfies all constraints + + return True # No combination of this specific length satisfied all constraints + def minimal_expl(self, trace): """ @@ -182,9 +201,14 @@ def counterfactual_expl(self, trace): new_explainer.add_constraint(self.constraints[idx]) return new_explainer.counterfactual_expl(trace) if self.minimal_solution: - self.contradiction( - True, len(trace) + 1 - ) # If the solution should be minimal, calculate all possible solutions + self.adherent_trace = None + length_of_trace = len(trace) + delta = 1 # Starting with an increment of 1 + while not self.adherent_trace: + self.contradiction_by_length(length_of_trace) + length_of_trace += delta + delta *= -1 # Alternate between adding 1 and subtracting 1 + if self.conformant(trace): return "The trace is already conformant, no changes needed." score = self.evaluate_similarity(trace) @@ -297,21 +321,9 @@ def evaluate_similarity(self, trace): :param trace: The trace to compare with the adherent trace. :return: A normalized score indicating the similarity between the adherent trace and the given trace. """ - length = 0 trace_len = len("".join(trace)) - lev_distance = 0 - if self.minimal_solution: - lev_distance = ( - len(max(self.adherent_traces)) + trace_len - ) # The maximum possible levenshtein distance - length = len(max(self.adherent_traces)) - for t in self.adherent_traces: - tmp_dist = levenshtein_distance(t, "".join(trace)) - if lev_distance > tmp_dist: # Closer to a possible solution - lev_distance = tmp_dist - else: - length = len(self.adherent_trace) - lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) + length = len(self.adherent_trace) + lev_distance = levenshtein_distance(self.adherent_trace, "".join(trace)) max_distance = max(length, trace_len) normalized_score = 1 - lev_distance / max_distance return normalized_score From dfce47ae5e509ce8c21133cd67c4500d55ef8645 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 23 Apr 2024 17:28:47 +0200 Subject: [PATCH 31/54] Updated tutorial --- explainer/explainer_regex.py | 4 +- explainer/tutorial/explainer_tutorial.ipynb | 69 ++++++++++----------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index e830874..815059e 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -139,6 +139,8 @@ def contradiction_by_length(self, length): :param length: The specific length of combinations to test. :return: Boolean indicating if there is a contradiction. """ + if length <= 0: + return True nodes = self.get_nodes_from_constraint() nodes = nodes + nodes # Assuming you need to double the nodes as in your previous snippet @@ -203,12 +205,12 @@ def counterfactual_expl(self, trace): if self.minimal_solution: self.adherent_trace = None length_of_trace = len(trace) + print(length_of_trace) delta = 1 # Starting with an increment of 1 while not self.adherent_trace: self.contradiction_by_length(length_of_trace) length_of_trace += delta delta *= -1 # Alternate between adding 1 and subtracting 1 - if self.conformant(trace): return "The trace is already conformant, no changes needed." score = self.evaluate_similarity(trace) diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb index d2b2630..c6e8816 100644 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ b/explainer/tutorial/explainer_tutorial.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -59,7 +59,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -87,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -204,41 +204,38 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Example without minimal solution\n", - "--------------------------------\n", - "\n", - "Subtraction (Removed A from position 2): A->B->C->B\n", - "Subtraction (Removed B from position 3): A->B->C\n", - "\n", - "Example with minimal solution\n", - "--------------------------------\n", - "\n", - "Addition (Added C at position 5): A->B->A->C->B->C\n", - "\n", - "Example without minimal solution\n", - "--------------------------------\n", - "\n", - "Addition (Added A at position 1): C->A->B->A\n", - "Subtraction (Removed C from position 0): A->B->A\n", - "Addition (Added C at position 2): A->B->C->A\n", - "Subtraction (Removed A from position 3): A->B->C\n", - "\n", - "Example with minimal solution\n", - "--------------------------------\n", - "\n", - "Addition (Added A at position 0): A->C->B->A\n", - "Addition (Added C at position 4): A->C->B->A->C\n" + "hej\n" + ] + }, + { + "ename": "TypeError", + "evalue": "'NoneType' object is not iterable", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 4\u001b[0m\n\u001b[1;32m 2\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mABCDE\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhej\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m----> 4\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mexp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcounterfactual_expl\u001b[49m\u001b[43m(\u001b[49m\u001b[43mTrace\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mA\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m 5\u001b[0m exp \u001b[38;5;241m=\u001b[39m ExplainerRegex()\n\u001b[1;32m 6\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m^A\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:218\u001b[0m, in \u001b[0;36mExplainerRegex.counterfactual_expl\u001b[0;34m(self, trace)\u001b[0m\n\u001b[1;32m 216\u001b[0m score \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mevaluate_similarity(trace)\n\u001b[1;32m 217\u001b[0m \u001b[38;5;66;03m# Perform operation based on the lowest scoring heuristic\u001b[39;00m\n\u001b[0;32m--> 218\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43moperate_on_trace\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtrace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mscore\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:260\u001b[0m, in \u001b[0;36mExplainerRegex.operate_on_trace\u001b[0;34m(self, trace, score, explanation_path, depth)\u001b[0m\n\u001b[1;32m 258\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39moperate_on_trace(subtrace[\u001b[38;5;241m0\u001b[39m], score, explanation_path, depth \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m 259\u001b[0m explanation_string \u001b[38;5;241m=\u001b[39m explanation_path \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(explanation)\n\u001b[0;32m--> 260\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcounter_factual_helper\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbest_subtrace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexplanation_string\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdepth\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:229\u001b[0m, in \u001b[0;36mExplainerRegex.counter_factual_helper\u001b[0;34m(self, working_trace, explanation, depth)\u001b[0m\n\u001b[1;32m 220\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcounter_factual_helper\u001b[39m(\u001b[38;5;28mself\u001b[39m, working_trace, explanation, depth\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m):\n\u001b[1;32m 221\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 222\u001b[0m \u001b[38;5;124;03m Recursively explores counterfactual explanations for a working trace.\u001b[39;00m\n\u001b[1;32m 223\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 227\u001b[0m \u001b[38;5;124;03m :return: A string explaining why the working trace is non-conformant or a message indicating the maximum depth has been reached.\u001b[39;00m\n\u001b[1;32m 228\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 229\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconformant\u001b[49m\u001b[43m(\u001b[49m\u001b[43mworking_trace\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 230\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mexplanation\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 231\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m depth \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m100\u001b[39m:\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:107\u001b[0m, in \u001b[0;36mExplainerRegex.conformant\u001b[0;34m(self, trace, constraints)\u001b[0m\n\u001b[1;32m 100\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mconformant\u001b[39m(\u001b[38;5;28mself\u001b[39m, trace, constraints\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 101\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 102\u001b[0m \u001b[38;5;124;03m Checks if the trace is conformant according to all the constraints.\u001b[39;00m\n\u001b[1;32m 103\u001b[0m \n\u001b[1;32m 104\u001b[0m \u001b[38;5;124;03m :param trace: A Trace instance.\u001b[39;00m\n\u001b[1;32m 105\u001b[0m \u001b[38;5;124;03m :return: Boolean indicating if the trace is conformant with all constraints.\u001b[39;00m\n\u001b[1;32m 106\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 107\u001b[0m activation \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mactivation\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtrace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconstraints\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 108\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28many\u001b[39m(value \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m value \u001b[38;5;129;01min\u001b[39;00m activation):\n\u001b[1;32m 109\u001b[0m new_explainer \u001b[38;5;241m=\u001b[39m ExplainerRegex()\n", + "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:72\u001b[0m, in \u001b[0;36mExplainerRegex.activation\u001b[0;34m(self, trace, constraints)\u001b[0m\n\u001b[1;32m 70\u001b[0m con_activation[idx] \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n\u001b[1;32m 71\u001b[0m \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[0;32m---> 72\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m event \u001b[38;5;129;01min\u001b[39;00m trace:\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m event \u001b[38;5;129;01min\u001b[39;00m con:\n\u001b[1;32m 74\u001b[0m con_activation[idx] \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n", + "\u001b[0;31mTypeError\u001b[0m: 'NoneType' object is not iterable" ] } ], "source": [ + "\"\"\"exp = ExplainerRegex()\n", + "exp.add_constraint(\"ABCDE\")\n", + "print(\"hej\")\n", + "print(exp.counterfactual_expl(Trace(['A'])))\n", "exp = ExplainerRegex()\n", "exp.add_constraint(\"^A\")\n", "exp.add_constraint(\"A.*B.*\")\n", @@ -261,7 +258,7 @@ "print(\"\\nExample with minimal solution\")\n", "print(\"--------------------------------\")\n", "exp.set_minimal_solution(True)\n", - "print(exp.counterfactual_expl(trace))" + "print(exp.counterfactual_expl(trace))\"\"\"" ] }, { @@ -281,7 +278,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -324,7 +321,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -358,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -396,7 +393,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -429,7 +426,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -453,7 +450,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [ { From 84a832eaeea07663125c93f0726c64fe688f2ef7 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 24 Apr 2024 08:39:56 +0200 Subject: [PATCH 32/54] Fixed weird bug --- explainer/explainer_regex.py | 2 +- explainer/tutorial/explainer_tutorial_1.ipynb | 548 ++++++++++++++++++ 2 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 explainer/tutorial/explainer_tutorial_1.ipynb diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 815059e..6106aac 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -289,7 +289,7 @@ def modify_subtrace(self, trace): for i, s_trace in enumerate(get_iterative_subtrace(trace)): for con in self.constraints: new_trace_str = "".join(s_trace) - match = re.match(new_trace_str, con) + match = re.search(con, new_trace_str) if not match: for add in possible_additions: potential_subtraces.append( diff --git a/explainer/tutorial/explainer_tutorial_1.ipynb b/explainer/tutorial/explainer_tutorial_1.ipynb new file mode 100644 index 0000000..2fa3fe7 --- /dev/null +++ b/explainer/tutorial/explainer_tutorial_1.ipynb @@ -0,0 +1,548 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Explainer utility in BPMN2CONSTRAINTS\n", + "\n", + "In this notebook, we explore the `Explainer` class, designed to analyze and explain the conformance of traces against predefined constraints. Trace analysis is crucial in domains such as process mining, where understanding the behavior of system executions against expected models can uncover inefficiencies, deviations, or compliance issues.\n", + "\n", + "The constraints currently consists of basic regex, this is because of it's similiarities and likeness to declarative constraints used in BPMN2CONSTRAINTS\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('../')\n", + "from explainer import Explainer, Trace, EventLog\n", + "from explainer_regex import ExplainerRegex\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Basic Usage\n", + "Let's start by creating an instance of the `Explainer` and adding a simple constraint that a valid trace should contain the sequence \"A\" followed by \"B\" and then \"C\".\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "explainer = ExplainerRegex()\n", + "explainer.add_constraint('A.*B.*C')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Analyzing Trace Conformance\n", + "\n", + "Now, we'll create a trace and check if it conforms to the constraints we've defined." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Is the trace conformant? True\n" + ] + } + ], + "source": [ + "trace = Trace(['A', 'X', 'B', 'Y', 'C'])\n", + "is_conformant = explainer.conformant(trace)\n", + "print(f\"Is the trace conformant? {is_conformant}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Explaining Non-conformance\n", + "\n", + "If a trace is not conformant, we can use the `minimal_expl` and `counterfactual_expl` methods to understand why and how to adjust the trace.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constraint: A.*B.*C\n", + "Trace:['A', 'C']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", + "\n", + "Addition (Added B at position 1): A->B->C\n", + "-----------\n", + "Constraint: A.*B.*C\n", + "Trace:['C', 'B', 'A']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", + "\n", + "Addition (Added A at position 1): C->A->B->A\n", + "Subtraction (Removed C from position 0): A->B->A\n", + "Addition (Added C at position 2): A->B->C->A\n", + "-----------\n", + "Constraint: A.*B.*C\n", + "Trace:['A', 'A', 'C']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", + "\n", + "Addition (Added B at position 1): A->B->A->C\n", + "-----------\n", + "Constraint: A.*B.*C\n", + "Trace:['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", + "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", + "\n", + "Subtraction (Removed TEST from position 4): A->A->C->A->A->C->X->Y\n", + "Addition (Added B at position 1): A->B->A->C->A->A->C->X->Y\n", + "-----------\n", + "Constraint: AC\n", + "Trace:['A', 'X', 'C']\n", + "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", + "\n", + "Subtraction (Removed X from position 1): A->C\n", + "-----------\n", + "constraint: AC\n", + "constraint: B.*A.*B.*C\n", + "constraint: A.*B.*C.*\n", + "constraint: A.*D.*B*\n", + "constraint: A[^D]*B\n", + "constraint: B.*[^X].*\n", + "Trace:['A', 'X', 'C']\n", + "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", + "\n", + "Subtraction (Removed X from position 1): A->C\n" + ] + } + ], + "source": [ + "non_conformant_trace = Trace(['A', 'C'])\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "\n", + "non_conformant_trace = Trace(['C', 'B', 'A'])\n", + "print('-----------')\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "\n", + "non_conformant_trace = Trace(['A','A','C'])\n", + "print('-----------')\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "\n", + "\n", + "non_conformant_trace = Trace(['A','A','C','A','TEST','A','C', 'X', 'Y']) \n", + "print('-----------')\n", + "print('Constraint: A.*B.*C')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "\n", + "\n", + "explainer.remove_constraint(0)\n", + "explainer.add_constraint('AC')\n", + "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", + "print('-----------')\n", + "print('Constraint: AC')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "print('-----------')\n", + "\n", + "explainer.add_constraint('B.*A.*B.*C')\n", + "explainer.add_constraint('A.*B.*C.*')\n", + "explainer.add_constraint('A.*D.*B*')\n", + "explainer.add_constraint('A[^D]*B')\n", + "explainer.add_constraint('B.*[^X].*')\n", + "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", + "for con in explainer.constraints:\n", + " print(f'constraint: {con}')\n", + "print('Trace:' + str(non_conformant_trace.nodes))\n", + "print(explainer.minimal_expl(non_conformant_trace))\n", + "print(explainer.counterfactual_expl(non_conformant_trace))\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: Generating minimal solutions" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hej\n", + "\n", + "Addition (Added E at position 1): A->E\n", + "Addition (Added D at position 1): A->D->E\n", + "Addition (Added C at position 1): A->C->D->E\n", + "Addition (Added B at position 1): A->B->C->D->E\n", + "Example without minimal solution\n", + "--------------------------------\n", + "\n", + "Subtraction (Removed A from position 2): A->B->C->B\n", + "Subtraction (Removed B from position 3): A->B->C\n", + "\n", + "Example with minimal solution\n", + "--------------------------------\n", + "5\n", + "\n", + "Addition (Added C at position 5): A->B->A->C->B->C\n", + "\n", + "Example without minimal solution\n", + "--------------------------------\n", + "\n", + "Addition (Added C at position 1): C->C->B->A\n", + "Addition (Added A at position 0): A->C->C->B->A\n", + "Addition (Added C at position 4): A->C->C->B->C->A\n", + "Subtraction (Removed A from position 5): A->C->C->B->C\n", + "\n", + "Example with minimal solution\n", + "--------------------------------\n", + "3\n", + "\n", + "Addition (Added A at position 1): C->A->B->A\n", + "Subtraction (Removed C from position 0): A->B->A\n", + "Addition (Added C at position 2): A->B->C->A\n", + "Subtraction (Removed A from position 3): A->B->C\n" + ] + } + ], + "source": [ + "exp = ExplainerRegex()\n", + "exp.add_constraint(\"^A\")\n", + "exp.add_constraint(\"A.*B.*\")\n", + "exp.add_constraint(\"C$\")\n", + "trace = Trace(['A', 'B','A','C', 'B'])\n", + "print(\"Example without minimal solution\")\n", + "print(\"--------------------------------\")\n", + "print(exp.counterfactual_expl(trace))\n", + "\n", + "print(\"\\nExample with minimal solution\")\n", + "print(\"--------------------------------\")\n", + "exp.set_minimal_solution(True)\n", + "print(exp.counterfactual_expl(trace))\n", + "exp.set_minimal_solution(False)\n", + "trace = Trace(['C','B','A'])\n", + "print(\"\\nExample without minimal solution\")\n", + "print(\"--------------------------------\")\n", + "print(exp.counterfactual_expl(trace))\n", + "\n", + "print(\"\\nExample with minimal solution\")\n", + "print(\"--------------------------------\")\n", + "exp.set_minimal_solution(True)\n", + "print(exp.counterfactual_expl(trace))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 6: Contribution functions and Event Logs\n", + "\n", + "For this project, 4 contribution functions have been developed to determined a trace variant's, or constraint's contribution to a system.\n", + "\n", + "For the sake efficiency, all of the contribution functions, `variant_ctrb_to_conformance_loss`, `variant_ctrb_to_fitness`,`constraint_ctrb_to_fitness` and `constraint_ctrb_to_conformance`, should equal the total amount of conformance loss and fitness rate.\n", + "\n", + "There are to methods to determine the conformance rate (and conformance loss, by extension) and the fitness rate; `determine_conformance_rate` and `determine_fitness_rate`. \n", + "\n", + "All of these methods utilized an abstraction of an Event Log. In this block, the initialization and usage of conformance rate and fitness rate is displayed." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conformance rate: 20.0%\n", + "Fitness rate: 50.0%\n" + ] + } + ], + "source": [ + "exp = ExplainerRegex()\n", + "# Setup an event log\n", + "event_log = EventLog()\n", + "traces = [\n", + " Trace(['A', 'B','C']),\n", + " Trace(['A', 'B']),\n", + " Trace(['B']),\n", + " Trace(['B','C'])\n", + "]\n", + "event_log.add_trace(traces[0], 10) # The second parameter is how many variants you'd like to add, leave blank for 1\n", + "event_log.add_trace(traces[1], 10)\n", + "event_log.add_trace(traces[2], 10)\n", + "event_log.add_trace(traces[3], 20)\n", + "# Add the constraints\n", + "exp.add_constraint('^A')\n", + "exp.add_constraint('C$')\n", + "\n", + "print(\"Conformance rate: \" + str(exp.determine_conformance_rate(event_log) * 100) + \"%\")\n", + "print(\"Fitness rate: \" + str(exp.determine_fitness_rate(event_log) * 100) + \"%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_conformance_loss` determines how much a specific variant contributes to the overall conformance loss" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution of variant to conformance rate\n", + "Ctrb of variant ['A', 'B', 'C']: 0.0\n", + "Ctrb of variant ['A', 'B']: 0.2\n", + "Ctrb of variant ['B']: 0.2\n", + "Ctrb of variant ['B', 'C']: 0.4\n", + "Total conformance loss: 0.8\n" + ] + } + ], + "source": [ + "print(\"Contribution of variant to conformance rate\")\n", + "print(\"Ctrb of variant \"+ str(traces[0].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[0])))\n", + "print(\"Ctrb of variant \"+ str(traces[1].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[1])))\n", + "print(\"Ctrb of variant \"+ str(traces[2].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[2])))\n", + "print(\"Ctrb of variant \"+ str(traces[3].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[3])))\n", + "print(\"Total conformance loss: \" + str(exp.variant_ctrb_to_conformance_loss(event_log, traces[0]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[1]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[2]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[3])))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_fitness` determines how much a specific variant contributes to the overall fitness rate" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution of variant to fitness rate\n", + "Ctrb of variant ['A', 'B', 'C']: 0.2\n", + "Ctrb of variant ['A', 'B']: 0.1\n", + "Ctrb of variant ['B']: 0.0\n", + "Ctrb of variant ['B', 'C']: 0.2\n", + "Total fitness: 0.5\n" + ] + } + ], + "source": [ + "print(\"Contribution of variant to fitness rate\")\n", + "print(\"Ctrb of variant \" + str(traces[0].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[0]), 2)))\n", + "print(\"Ctrb of variant \" + str(traces[1].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[1]), 2)))\n", + "print(\"Ctrb of variant \" + str(traces[2].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[2]), 2)))\n", + "print(\"Ctrb of variant \" + str(traces[3].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[3]), 2)))\n", + "total_fitness = (exp.variant_ctrb_to_fitness(event_log, traces[0]) +\n", + " exp.variant_ctrb_to_fitness(event_log, traces[1]) +\n", + " exp.variant_ctrb_to_fitness(event_log, traces[2]) +\n", + " exp.variant_ctrb_to_fitness(event_log, traces[3]))\n", + "print(\"Total fitness: \" + str(round(total_fitness, 2)))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`constraint_ctrb_to_fitness` determines how much a specific constraint contributes to the overall fitness rate" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "^A ctrb to fitness rate: 0.2\n", + "B$ ctrb to fitness rate: 0.3\n", + "Total fitness: 0.5\n" + ] + } + ], + "source": [ + "\n", + "print(\"^A ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0)))\n", + "print(\"B$ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))\n", + "print(\"Total fitness: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 7: Shapely values\n", + "\n", + "`constraint_ctrb_to_conformance` determines how much a specific constraint contributes to the overall conformance loss. \n", + "\n", + "Because the constraints overlap in this case, Shapley values have been used to determine the contribution. This makes the method more complicated and more computationally heavy than the other contribution functions \n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contriution of constraint to conformance rate\n", + "^A ctrb: 0.5\n", + "C$ ctrb: 0.30000000000000004 (adjusted 0.3)\n", + "Total conformance loss: 0.8\n" + ] + } + ], + "source": [ + "print(\"Contriution of constraint to conformance rate\")\n", + "print(\"^A ctrb: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0)))\n", + "print(\"C$ ctrb: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1)) + \" (adjusted \" + str(round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1), 2)) + \")\")\n", + "print(\"Total conformance loss: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1)))\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conformance rate: 0.14\n", + "Contribution C$: 0.21\n", + "Contribution ^A: 0.36\n", + "Contribution B+: 0.29\n", + "Conformance loss = 86.0%, contribution to loss: 86.0%\n", + "------------------------------------\n", + "Fitness rate: 0.6666666666666666\n", + "C$ ctrb to fitness rate: 0.23809523809523808\n", + "^A ctrb to fitness rate: 0.19047619047619047\n", + "B+ ctrb to fitness rate: 0.23809523809523808\n", + "Total fitness: 0.6666666666666666\n" + ] + } + ], + "source": [ + "exp = ExplainerRegex()\n", + "event_log = EventLog()\n", + "trace1 = Trace(['A', 'B', 'C'])\n", + "trace2 = Trace(['B', 'C'])\n", + "trace3 = Trace(['A', 'B'])\n", + "trace4 = Trace(['B'])\n", + "trace5 = Trace(['A', 'C'])\n", + "\n", + "\n", + "event_log.add_trace(trace1, 5) \n", + "event_log.add_trace(trace2, 10)\n", + "event_log.add_trace(trace3, 5)\n", + "event_log.add_trace(trace4, 5)\n", + "event_log.add_trace(trace5, 10)\n", + "\n", + "\n", + "exp = ExplainerRegex()\n", + "exp.add_constraint(\"C$\")\n", + "exp.add_constraint(\"^A\")\n", + "exp.add_constraint(\"B+\")\n", + "conf_rate = exp.determine_conformance_rate(event_log)\n", + "print(\"Conformance rate: \"+ str(round(conf_rate, 2)))\n", + "print(\"Contribution C$: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0), 2)) # Round for easier readability\n", + "print(\"Contribution ^A: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1), 2))\n", + "print(\"Contribution B+: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 2), 2))\n", + "total_ctrb = exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 2)\n", + "conf_rate = round(conf_rate, 2) \n", + "total_ctrb = round(total_ctrb, 2)\n", + "print(\"Conformance loss = \" + str(100 - (conf_rate * 100)) + \"%, contribution to loss: \" + str(total_ctrb * 100) + \"%\")\n", + "print(\"------------------------------------\")\n", + "print(\"Fitness rate: \"+ str(exp.determine_fitness_rate(event_log)))\n", + "print(\"C$ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0)))\n", + "print(\"^A ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))\n", + "print(\"B+ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 2)))\n", + "\n", + "print(\"Total fitness: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 2)))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 3b17ada0f42e127ffbf50ec458020026251d82b6 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Wed, 24 Apr 2024 08:40:39 +0200 Subject: [PATCH 33/54] Ran notebook again --- explainer/tutorial/explainer_tutorial.ipynb | 535 ------------------ explainer/tutorial/explainer_tutorial_1.ipynb | 19 +- 2 files changed, 8 insertions(+), 546 deletions(-) delete mode 100644 explainer/tutorial/explainer_tutorial.ipynb diff --git a/explainer/tutorial/explainer_tutorial.ipynb b/explainer/tutorial/explainer_tutorial.ipynb deleted file mode 100644 index c6e8816..0000000 --- a/explainer/tutorial/explainer_tutorial.ipynb +++ /dev/null @@ -1,535 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Explainer utility in BPMN2CONSTRAINTS\n", - "\n", - "In this notebook, we explore the `Explainer` class, designed to analyze and explain the conformance of traces against predefined constraints. Trace analysis is crucial in domains such as process mining, where understanding the behavior of system executions against expected models can uncover inefficiencies, deviations, or compliance issues.\n", - "\n", - "The constraints currently consists of basic regex, this is because of it's similiarities and likeness to declarative constraints used in BPMN2CONSTRAINTS\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 1: Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "sys.path.append('../')\n", - "from explainer import Explainer, Trace, EventLog\n", - "from explainer_regex import ExplainerRegex\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 2: Basic Usage\n", - "Let's start by creating an instance of the `Explainer` and adding a simple constraint that a valid trace should contain the sequence \"A\" followed by \"B\" and then \"C\".\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "explainer = ExplainerRegex()\n", - "explainer.add_constraint('A.*B.*C')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 3: Analyzing Trace Conformance\n", - "\n", - "Now, we'll create a trace and check if it conforms to the constraints we've defined." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Is the trace conformant? True\n" - ] - } - ], - "source": [ - "trace = Trace(['A', 'X', 'B', 'Y', 'C'])\n", - "is_conformant = explainer.conformant(trace)\n", - "print(f\"Is the trace conformant? {is_conformant}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 4: Explaining Non-conformance\n", - "\n", - "If a trace is not conformant, we can use the `minimal_expl` and `counterfactual_expl` methods to understand why and how to adjust the trace.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Constraint: A.*B.*C\n", - "Trace:['A', 'C']\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'C')\n", - "\n", - "Addition (Added B at position 1): A->B->C\n", - "-----------\n", - "Constraint: A.*B.*C\n", - "Trace:['C', 'B', 'A']\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('C', 'B')\n", - "\n", - "Addition (Added A at position 1): C->A->B->A\n", - "Subtraction (Removed C from position 0): A->B->A\n", - "Addition (Added C at position 2): A->B->C->A\n", - "-----------\n", - "Constraint: A.*B.*C\n", - "Trace:['A', 'A', 'C']\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", - "\n", - "Addition (Added B at position 2): A->A->B->C\n", - "-----------\n", - "Constraint: A.*B.*C\n", - "Trace:['A', 'A', 'C', 'A', 'TEST', 'A', 'C', 'X', 'Y']\n", - "Non-conformance due to: Constraint (A.*B.*C) is violated by subtrace: ('A', 'A')\n", - "\n", - "Subtraction (Removed TEST from position 4): A->A->C->A->A->C->X->Y\n", - "Addition (Added B at position 2): A->A->B->C->A->A->C->X->Y\n", - "-----------\n", - "Constraint: AC\n", - "Trace:['A', 'X', 'C']\n", - "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", - "\n", - "Subtraction (Removed X from position 1): A->C\n", - "-----------\n", - "constraint: AC\n", - "constraint: B.*A.*B.*C\n", - "constraint: A.*B.*C.*\n", - "constraint: A.*D.*B*\n", - "constraint: A[^D]*B\n", - "constraint: B.*[^X].*\n", - "Trace:['A', 'X', 'C']\n", - "Non-conformance due to: Constraint (AC) is violated by subtrace: ('A', 'X')\n", - "\n", - "Subtraction (Removed X from position 1): A->C\n" - ] - } - ], - "source": [ - "non_conformant_trace = Trace(['A', 'C'])\n", - "print('Constraint: A.*B.*C')\n", - "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.minimal_expl(non_conformant_trace))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", - "\n", - "non_conformant_trace = Trace(['C', 'B', 'A'])\n", - "print('-----------')\n", - "print('Constraint: A.*B.*C')\n", - "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.minimal_expl(non_conformant_trace))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", - "\n", - "non_conformant_trace = Trace(['A','A','C'])\n", - "print('-----------')\n", - "print('Constraint: A.*B.*C')\n", - "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.minimal_expl(non_conformant_trace))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", - "\n", - "\n", - "non_conformant_trace = Trace(['A','A','C','A','TEST','A','C', 'X', 'Y']) \n", - "print('-----------')\n", - "print('Constraint: A.*B.*C')\n", - "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.minimal_expl(non_conformant_trace))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", - "\n", - "\n", - "explainer.remove_constraint(0)\n", - "explainer.add_constraint('AC')\n", - "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", - "print('-----------')\n", - "print('Constraint: AC')\n", - "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.minimal_expl(non_conformant_trace))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", - "print('-----------')\n", - "\n", - "explainer.add_constraint('B.*A.*B.*C')\n", - "explainer.add_constraint('A.*B.*C.*')\n", - "explainer.add_constraint('A.*D.*B*')\n", - "explainer.add_constraint('A[^D]*B')\n", - "explainer.add_constraint('B.*[^X].*')\n", - "non_conformant_trace = Trace(['A', 'X', 'C']) #Substraction\n", - "for con in explainer.constraints:\n", - " print(f'constraint: {con}')\n", - "print('Trace:' + str(non_conformant_trace.nodes))\n", - "print(explainer.minimal_expl(non_conformant_trace))\n", - "print(explainer.counterfactual_expl(non_conformant_trace))\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 5: Generating minimal solutions" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "hej\n" - ] - }, - { - "ename": "TypeError", - "evalue": "'NoneType' object is not iterable", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[10], line 4\u001b[0m\n\u001b[1;32m 2\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mABCDE\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhej\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m----> 4\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mexp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcounterfactual_expl\u001b[49m\u001b[43m(\u001b[49m\u001b[43mTrace\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mA\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m 5\u001b[0m exp \u001b[38;5;241m=\u001b[39m ExplainerRegex()\n\u001b[1;32m 6\u001b[0m exp\u001b[38;5;241m.\u001b[39madd_constraint(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m^A\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:218\u001b[0m, in \u001b[0;36mExplainerRegex.counterfactual_expl\u001b[0;34m(self, trace)\u001b[0m\n\u001b[1;32m 216\u001b[0m score \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mevaluate_similarity(trace)\n\u001b[1;32m 217\u001b[0m \u001b[38;5;66;03m# Perform operation based on the lowest scoring heuristic\u001b[39;00m\n\u001b[0;32m--> 218\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43moperate_on_trace\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtrace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mscore\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:260\u001b[0m, in \u001b[0;36mExplainerRegex.operate_on_trace\u001b[0;34m(self, trace, score, explanation_path, depth)\u001b[0m\n\u001b[1;32m 258\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39moperate_on_trace(subtrace[\u001b[38;5;241m0\u001b[39m], score, explanation_path, depth \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m 259\u001b[0m explanation_string \u001b[38;5;241m=\u001b[39m explanation_path \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(explanation)\n\u001b[0;32m--> 260\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcounter_factual_helper\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbest_subtrace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexplanation_string\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdepth\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:229\u001b[0m, in \u001b[0;36mExplainerRegex.counter_factual_helper\u001b[0;34m(self, working_trace, explanation, depth)\u001b[0m\n\u001b[1;32m 220\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcounter_factual_helper\u001b[39m(\u001b[38;5;28mself\u001b[39m, working_trace, explanation, depth\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m):\n\u001b[1;32m 221\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 222\u001b[0m \u001b[38;5;124;03m Recursively explores counterfactual explanations for a working trace.\u001b[39;00m\n\u001b[1;32m 223\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 227\u001b[0m \u001b[38;5;124;03m :return: A string explaining why the working trace is non-conformant or a message indicating the maximum depth has been reached.\u001b[39;00m\n\u001b[1;32m 228\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 229\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconformant\u001b[49m\u001b[43m(\u001b[49m\u001b[43mworking_trace\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 230\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mexplanation\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 231\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m depth \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m100\u001b[39m:\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:107\u001b[0m, in \u001b[0;36mExplainerRegex.conformant\u001b[0;34m(self, trace, constraints)\u001b[0m\n\u001b[1;32m 100\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mconformant\u001b[39m(\u001b[38;5;28mself\u001b[39m, trace, constraints\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 101\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 102\u001b[0m \u001b[38;5;124;03m Checks if the trace is conformant according to all the constraints.\u001b[39;00m\n\u001b[1;32m 103\u001b[0m \n\u001b[1;32m 104\u001b[0m \u001b[38;5;124;03m :param trace: A Trace instance.\u001b[39;00m\n\u001b[1;32m 105\u001b[0m \u001b[38;5;124;03m :return: Boolean indicating if the trace is conformant with all constraints.\u001b[39;00m\n\u001b[1;32m 106\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 107\u001b[0m activation \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mactivation\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtrace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconstraints\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 108\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28many\u001b[39m(value \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m value \u001b[38;5;129;01min\u001b[39;00m activation):\n\u001b[1;32m 109\u001b[0m new_explainer \u001b[38;5;241m=\u001b[39m ExplainerRegex()\n", - "File \u001b[0;32m~/sap/bpmn2constraints/explainer/tutorial/../explainer_regex.py:72\u001b[0m, in \u001b[0;36mExplainerRegex.activation\u001b[0;34m(self, trace, constraints)\u001b[0m\n\u001b[1;32m 70\u001b[0m con_activation[idx] \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n\u001b[1;32m 71\u001b[0m \u001b[38;5;28;01mcontinue\u001b[39;00m\n\u001b[0;32m---> 72\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m event \u001b[38;5;129;01min\u001b[39;00m trace:\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m event \u001b[38;5;129;01min\u001b[39;00m con:\n\u001b[1;32m 74\u001b[0m con_activation[idx] \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n", - "\u001b[0;31mTypeError\u001b[0m: 'NoneType' object is not iterable" - ] - } - ], - "source": [ - "\"\"\"exp = ExplainerRegex()\n", - "exp.add_constraint(\"ABCDE\")\n", - "print(\"hej\")\n", - "print(exp.counterfactual_expl(Trace(['A'])))\n", - "exp = ExplainerRegex()\n", - "exp.add_constraint(\"^A\")\n", - "exp.add_constraint(\"A.*B.*\")\n", - "exp.add_constraint(\"C$\")\n", - "trace = Trace(['A', 'B','A','C', 'B'])\n", - "print(\"Example without minimal solution\")\n", - "print(\"--------------------------------\")\n", - "print(exp.counterfactual_expl(trace))\n", - "\n", - "print(\"\\nExample with minimal solution\")\n", - "print(\"--------------------------------\")\n", - "exp.set_minimal_solution(True)\n", - "print(exp.counterfactual_expl(trace))\n", - "exp.set_minimal_solution(False)\n", - "trace = Trace(['C','B','A'])\n", - "print(\"\\nExample without minimal solution\")\n", - "print(\"--------------------------------\")\n", - "print(exp.counterfactual_expl(trace))\n", - "\n", - "print(\"\\nExample with minimal solution\")\n", - "print(\"--------------------------------\")\n", - "exp.set_minimal_solution(True)\n", - "print(exp.counterfactual_expl(trace))\"\"\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 6: Contribution functions and Event Logs\n", - "\n", - "For this project, 4 contribution functions have been developed to determined a trace variant's, or constraint's contribution to a system.\n", - "\n", - "For the sake efficiency, all of the contribution functions, `variant_ctrb_to_conformance_loss`, `variant_ctrb_to_fitness`,`constraint_ctrb_to_fitness` and `constraint_ctrb_to_conformance`, should equal the total amount of conformance loss and fitness rate.\n", - "\n", - "There are to methods to determine the conformance rate (and conformance loss, by extension) and the fitness rate; `determine_conformance_rate` and `determine_fitness_rate`. \n", - "\n", - "All of these methods utilized an abstraction of an Event Log. In this block, the initialization and usage of conformance rate and fitness rate is displayed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Conformance rate: 20.0%\n", - "Fitness rate: 50.0%\n" - ] - } - ], - "source": [ - "exp = ExplainerRegex()\n", - "# Setup an event log\n", - "event_log = EventLog()\n", - "traces = [\n", - " Trace(['A', 'B','C']),\n", - " Trace(['A', 'B']),\n", - " Trace(['B']),\n", - " Trace(['B','C'])\n", - "]\n", - "event_log.add_trace(traces[0], 10) # The second parameter is how many variants you'd like to add, leave blank for 1\n", - "event_log.add_trace(traces[1], 10)\n", - "event_log.add_trace(traces[2], 10)\n", - "event_log.add_trace(traces[3], 20)\n", - "# Add the constraints\n", - "exp.add_constraint('^A')\n", - "exp.add_constraint('C$')\n", - "\n", - "print(\"Conformance rate: \" + str(exp.determine_conformance_rate(event_log) * 100) + \"%\")\n", - "print(\"Fitness rate: \" + str(exp.determine_fitness_rate(event_log) * 100) + \"%\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`variant_ctrb_to_conformance_loss` determines how much a specific variant contributes to the overall conformance loss" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Contribution of variant to conformance rate\n", - "Ctrb of variant ['A', 'B', 'C']: 0.0\n", - "Ctrb of variant ['A', 'B']: 0.2\n", - "Ctrb of variant ['B']: 0.2\n", - "Ctrb of variant ['B', 'C']: 0.4\n", - "Total conformance loss: 0.8\n" - ] - } - ], - "source": [ - "print(\"Contribution of variant to conformance rate\")\n", - "print(\"Ctrb of variant \"+ str(traces[0].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[0])))\n", - "print(\"Ctrb of variant \"+ str(traces[1].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[1])))\n", - "print(\"Ctrb of variant \"+ str(traces[2].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[2])))\n", - "print(\"Ctrb of variant \"+ str(traces[3].nodes) +\": \"+ str(exp.variant_ctrb_to_conformance_loss(event_log, traces[3])))\n", - "print(\"Total conformance loss: \" + str(exp.variant_ctrb_to_conformance_loss(event_log, traces[0]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[1]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[2]) + exp.variant_ctrb_to_conformance_loss(event_log, traces[3])))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`variant_ctrb_to_fitness` determines how much a specific variant contributes to the overall fitness rate" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Contribution of variant to fitness rate\n", - "Ctrb of variant ['A', 'B', 'C']: 0.2\n", - "Ctrb of variant ['A', 'B']: 0.1\n", - "Ctrb of variant ['B']: 0.0\n", - "Ctrb of variant ['B', 'C']: 0.2\n", - "Total fitness: 0.5\n" - ] - } - ], - "source": [ - "print(\"Contribution of variant to fitness rate\")\n", - "print(\"Ctrb of variant \" + str(traces[0].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[0]), 2)))\n", - "print(\"Ctrb of variant \" + str(traces[1].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[1]), 2)))\n", - "print(\"Ctrb of variant \" + str(traces[2].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[2]), 2)))\n", - "print(\"Ctrb of variant \" + str(traces[3].nodes) + \": \" + str(round(exp.variant_ctrb_to_fitness(event_log, traces[3]), 2)))\n", - "total_fitness = (exp.variant_ctrb_to_fitness(event_log, traces[0]) +\n", - " exp.variant_ctrb_to_fitness(event_log, traces[1]) +\n", - " exp.variant_ctrb_to_fitness(event_log, traces[2]) +\n", - " exp.variant_ctrb_to_fitness(event_log, traces[3]))\n", - "print(\"Total fitness: \" + str(round(total_fitness, 2)))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`constraint_ctrb_to_fitness` determines how much a specific constraint contributes to the overall fitness rate" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "^A ctrb to fitness rate: 0.2\n", - "B$ ctrb to fitness rate: 0.3\n", - "Total fitness: 0.5\n" - ] - } - ], - "source": [ - "\n", - "print(\"^A ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0)))\n", - "print(\"B$ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))\n", - "print(\"Total fitness: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 7: Shapely values\n", - "\n", - "`constraint_ctrb_to_conformance` determines how much a specific constraint contributes to the overall conformance loss. \n", - "\n", - "Because the constraints overlap in this case, Shapley values have been used to determine the contribution. This makes the method more complicated and more computationally heavy than the other contribution functions \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Contriution of constraint to conformance rate\n", - "^A ctrb: 0.5\n", - "C$ ctrb: 0.30000000000000004 (adjusted 0.3)\n", - "Total conformance loss: 0.8\n" - ] - } - ], - "source": [ - "print(\"Contriution of constraint to conformance rate\")\n", - "print(\"^A ctrb: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0)))\n", - "print(\"C$ ctrb: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1)) + \" (adjusted \" + str(round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1), 2)) + \")\")\n", - "print(\"Total conformance loss: \" + str(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1)))\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Conformance rate: 0.14\n", - "Contribution C$: 0.21\n", - "Contribution ^A: 0.36\n", - "Contribution B+: 0.29\n", - "Conformance loss = 86.0%, contribution to loss: 86.0%\n", - "------------------------------------\n", - "Fitness rate: 0.6666666666666666\n", - "C$ ctrb to fitness rate: 0.23809523809523808\n", - "^A ctrb to fitness rate: 0.19047619047619047\n", - "B+ ctrb to fitness rate: 0.23809523809523808\n", - "Total fitness: 0.6666666666666666\n" - ] - } - ], - "source": [ - "exp = ExplainerRegex()\n", - "event_log = EventLog()\n", - "trace1 = Trace(['A', 'B', 'C'])\n", - "trace2 = Trace(['B', 'C'])\n", - "trace3 = Trace(['A', 'B'])\n", - "trace4 = Trace(['B'])\n", - "trace5 = Trace(['A', 'C'])\n", - "\n", - "\n", - "event_log.add_trace(trace1, 5) \n", - "event_log.add_trace(trace2, 10)\n", - "event_log.add_trace(trace3, 5)\n", - "event_log.add_trace(trace4, 5)\n", - "event_log.add_trace(trace5, 10)\n", - "\n", - "\n", - "exp = ExplainerRegex()\n", - "exp.add_constraint(\"C$\")\n", - "exp.add_constraint(\"^A\")\n", - "exp.add_constraint(\"B+\")\n", - "conf_rate = exp.determine_conformance_rate(event_log)\n", - "print(\"Conformance rate: \"+ str(round(conf_rate, 2)))\n", - "print(\"Contribution C$: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0), 2)) # Round for easier readability\n", - "print(\"Contribution ^A: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1), 2))\n", - "print(\"Contribution B+: \", round(exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 2), 2))\n", - "total_ctrb = exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 1) + exp.constraint_ctrb_to_conformance(event_log, exp.constraints, 2)\n", - "conf_rate = round(conf_rate, 2) \n", - "total_ctrb = round(total_ctrb, 2)\n", - "print(\"Conformance loss = \" + str(100 - (conf_rate * 100)) + \"%, contribution to loss: \" + str(total_ctrb * 100) + \"%\")\n", - "print(\"------------------------------------\")\n", - "print(\"Fitness rate: \"+ str(exp.determine_fitness_rate(event_log)))\n", - "print(\"C$ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0)))\n", - "print(\"^A ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1)))\n", - "print(\"B+ ctrb to fitness rate: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 2)))\n", - "\n", - "print(\"Total fitness: \" + str(exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 0) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 1) + exp.constraint_ctrb_to_fitness(event_log, exp.constraints, 2)))\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/explainer/tutorial/explainer_tutorial_1.ipynb b/explainer/tutorial/explainer_tutorial_1.ipynb index 2fa3fe7..62f0699 100644 --- a/explainer/tutorial/explainer_tutorial_1.ipynb +++ b/explainer/tutorial/explainer_tutorial_1.ipynb @@ -211,12 +211,6 @@ "name": "stdout", "output_type": "stream", "text": [ - "hej\n", - "\n", - "Addition (Added E at position 1): A->E\n", - "Addition (Added D at position 1): A->D->E\n", - "Addition (Added C at position 1): A->C->D->E\n", - "Addition (Added B at position 1): A->B->C->D->E\n", "Example without minimal solution\n", "--------------------------------\n", "\n", @@ -227,15 +221,18 @@ "--------------------------------\n", "5\n", "\n", - "Addition (Added C at position 5): A->B->A->C->B->C\n", + "Addition (Added B at position 3): A->B->A->B->C->B\n", + "Subtraction (Removed B from position 5): A->B->A->B->C\n", "\n", "Example without minimal solution\n", "--------------------------------\n", "\n", - "Addition (Added C at position 1): C->C->B->A\n", - "Addition (Added A at position 0): A->C->C->B->A\n", - "Addition (Added C at position 4): A->C->C->B->C->A\n", - "Subtraction (Removed A from position 5): A->C->C->B->C\n", + "Addition (Added A at position 1): C->A->B->A\n", + "Addition (Added A at position 1): C->A->A->B->A\n", + "Addition (Added A at position 1): C->A->A->A->B->A\n", + "Subtraction (Removed C from position 0): A->A->A->B->A\n", + "Addition (Added C at position 4): A->A->A->B->C->A\n", + "Subtraction (Removed A from position 5): A->A->A->B->C\n", "\n", "Example with minimal solution\n", "--------------------------------\n", From 25bad936f53b7e846bc727a43b0aca45b7e82b22 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 29 Apr 2024 10:21:17 +0200 Subject: [PATCH 34/54] Some progress --- explainer/SignavioAuthenticator.py | 42 ++++ explainer/explainer.py | 20 +- explainer/explainer_regex.py | 22 ++ explainer/explainer_signal.py | 212 +++++++++++++++++- explainer/tutorial/explainer_tutorial_2.ipynb | 25 +++ tests/explainer/explainer_test.py | 4 +- 6 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 explainer/SignavioAuthenticator.py create mode 100644 explainer/tutorial/explainer_tutorial_2.ipynb diff --git a/explainer/SignavioAuthenticator.py b/explainer/SignavioAuthenticator.py new file mode 100644 index 0000000..da86dc3 --- /dev/null +++ b/explainer/SignavioAuthenticator.py @@ -0,0 +1,42 @@ +import requests + + +class SignavioAuthenticator: + def __init__(self, system_instance, tenant_id, email, pw): + self.system_instance = system_instance + self.tenant_id = tenant_id + self.email = email + self.pw = pw + + """ + Takes care of authentication against Signavio systems + """ + + def authenticate(self): + """ + Authenticates user at Signavio system instance and initiates session. + Returns: + dictionary: Session information + """ + login_url = self.system_instance + "/p/login" + data = {"name": self.email, "password": self.pw, "tokenonly": "true"} + if "tenant_id" in locals(): + data["tenant"] = self.tenant_id + # authenticate + login_request = requests.post(login_url, data) + + # retrieve token and session ID + auth_token = login_request.content.decode("utf-8") + jsesssion_ID = login_request.cookies["JSESSIONID"] + + # The cookie is named 'LBROUTEID' for base_url 'editor.signavio.com' + # and 'editor.signavio.com', and 'AWSELB' for base_url + # 'app-au.signavio.com' and 'app-us.signavio.com' + lb_route_ID = login_request.cookies["LBROUTEID"] + + # return credentials + return { + "jsesssion_ID": jsesssion_ID, + "lb_route_ID": lb_route_ID, + "auth_token": auth_token, + } diff --git a/explainer/explainer.py b/explainer/explainer.py index 3744d14..d21f91b 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -194,6 +194,20 @@ def get_variant_count(self, trace): trace_tuple = tuple(trace.nodes) return self.log.get(trace_tuple, 0) + def get_most_frequent_variant(self): + """ + Returns the trace variant with the highest occurrence along with its count. + + :return: A tuple containing the most frequent trace as a Trace instance and its count. + """ + if not self.log: + return None, 0 # Return None and 0 if the log is empty + + # Find the trace with the maximum count + max_trace_tuple = max(self.log, key=self.log.get) + max_count = self.log[max_trace_tuple] + return Trace(list(max_trace_tuple)), max_count + def __str__(self): """ Returns a string representation of the event log. @@ -208,9 +222,9 @@ def __len__(self): def __iter__(self): """ - Allows iteration over each trace occurrence in the log. + Allows iteration over each trace occurrence in the log, sorted by count in descending order. """ - for trace_tuple, count in self.log.items(): + sorted_log = sorted(self.log.items(), key=lambda item: item[1], reverse=True) + for trace_tuple, count in sorted_log: for _ in range(count): yield Trace(list(trace_tuple)) - diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 6106aac..2910e94 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -358,6 +358,8 @@ def determine_fitness_rate(self, event_log, constraints = None): return "The explainer have no constraints" if constraints == None: constraints = self.constraints + if len(constraints) == 0: + print("hej") conformant = 0 for con in constraints: for trace, count in event_log.log.items(): @@ -523,3 +525,23 @@ def levenshtein_distance(seq1, seq2): matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 ) return matrix[size_x - 1][size_y - 1] + +exp = ExplainerRegex() + +traces = [ + Trace(['A', 'B','C']), + Trace(['A', 'B']), + Trace(['B']), + Trace(['B','C']) +] +event_log = EventLog() +event_log.add_trace(traces[0], 10) # The second parameter is how many variants you'd like to add, leave blank for 1 +event_log.add_trace(traces[1], 10) +event_log.add_trace(traces[2], 10) +event_log.add_trace(traces[3], 20) +exp.add_constraint('^A') +exp.add_constraint('C$') + +print(exp.determine_conformance_rate(event_log)) +print(exp.determine_fitness_rate(event_log)) +print(exp.constraint_ctrb_to_conformance(event_log, exp.constraints,0)) diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 8233b08..2b66eae 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -1,17 +1,24 @@ from abc import ABC, abstractmethod from explainer import Explainer, Trace, EventLog import re -from tutorial import SignavioAuthenticator +import math +import SignavioAuthenticator import json +from itertools import combinations, chain +import requests from conf import system_instance, workspace_id, user_name, pw class ExplainerSignal(Explainer): def __init__(self): super().__init__() - authenticator = SignavioAuthenticator(system_instance, workspace_id, user_name, pw) - auth_data = authenticator.authenticate() - cookies = {'JSESSIONID': auth_data['jsesssion_ID'], 'LBROUTEID': auth_data['lb_route_ID']} - headers = {'Accept': 'application/json', 'x-signavio-id': auth_data['auth_token']} - #diagram_url = system_instance + '/p/revision' + self.authenticator = SignavioAuthenticator.SignavioAuthenticator(system_instance, workspace_id, user_name, pw) + self.auth_data = self.authenticator.authenticate() + self.cookies = {'JSESSIONID': self.auth_data['jsesssion_ID'], 'LBROUTEID': self.auth_data['lb_route_ID']} + self.headers = {'Accept': 'application/json', 'x-signavio-id': self.auth_data['auth_token']} + self.signal_endpoint = system_instance + '/g/api/pi-graphql/signal' + self.event_log = EventLog() + self.load_variants() + self.minimal_expl(self.event_log.get_most_frequent_variant()) + def remove_constraint(self, idx): """ Removes a constraint by index and updates the nodes list if necessary. @@ -46,7 +53,7 @@ def contradiction(self, check_multiple=False, max_length=10): pass def minimal_expl(self, trace): - pass + print(trace) def counterfactual_expl(self, trace): pass @@ -57,13 +64,196 @@ def determine_shapley_value(self, log, constraints, index): def evaluate_similarity(self, trace): pass - def determine_conformance_rate(self, event_log, constraints=None): + def determine_conformance_rate(self, event_log = None, constraints=None): + if constraints == None: + constraints = self.constraints + if constraints == []: + return 1 + non_conformant = 0 + non_conformant = self.check_violations(constraints) + + len_log = self.get_total_cases() + + return (len_log - non_conformant) / len_log + + + def determine_fitness_rate(self, event_log = None, constraints = None): + if not constraints: + constraints = self.constraints + len_log = self.get_total_cases() + total_conformance = 0 + for con in constraints: + total_conformance += self.check_conformance(con) + + return total_conformance / (len_log * len(constraints)) + + + def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): + # Implementation remains the same pass - def trace_contribution_to_conformance_loss(self, event_log, trace, constraints=None): + def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): + # Implementation remains the same pass + + def constraint_ctrb_to_conformance(self, log = None, constraints = None, index = -1): + """Determines the Shapley value-based contribution of a constraint to a the + overall conformance rate. + Args: + log (dictionary): The event log, where keys are strings and values are + ints + constraints (list): A list of constraints (regexp strings) + index (int): The + Returns: + float: The contribution of the constraint to the overall conformance + rate + """ + if not constraints: + constraints = self.constraints + if len(constraints) < index: + raise Exception("Constraint not in constraint list.") + if index == -1: + return f"Add an index for the constraint:\n {constraints}" + contributor = constraints[index] + sub_ctrbs = [] + reduced_constraints = [c for c in constraints if not c == contributor] + subsets = determine_powerset(reduced_constraints) + for subset in subsets: + lsubset = list(subset) + constraints_without = [c for c in constraints if c in lsubset] + constraints_with = [c for c in constraints if c in lsubset + [contributor]] + weight = ( + math.factorial(len(lsubset)) + * math.factorial(len(constraints) - 1 - len(lsubset)) + ) / math.factorial(len(constraints)) + sub_ctrb = weight * ( + self.determine_conformance_rate(constraints=constraints_without) + - self.determine_conformance_rate(constraints=constraints_with) + ) + sub_ctrbs.append(sub_ctrb) + return sum(sub_ctrbs) + + def constraint_ctrb_to_fitness(self, log = None, constraints = None, index = -1): + # Implementation remains the same + if len(constraints) < index: + raise Exception("Constraint not in constraint list.") + if not constraints: + constraints = self.constraints + if index == -1: + return f"Add an index for the constraint:\n {constraints}" + contributor = constraints[index] + ctrb_count = self.check_conformance(contributor) + len_log = self.get_total_cases() + return ctrb_count / (len_log * len(constraints)) + + def check_conformance(self, constraint): + query_request = requests.post( + self.signal_endpoint, + cookies=self.cookies, + headers=self.headers, + json={'query': f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}'}) + return query_request.json()['data'][0][0] + + def check_conformances(self, constraints): + combined_constraints = " AND ".join([f"event_name MATCHES {constraint}" for constraint in constraints]) + query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' + + query_request = requests.post( + self.signal_endpoint, + cookies=self.cookies, + headers=self.headers, + json={'query': query} + ) + return query_request.json()['data'][0][0] + + def check_violations(self, constraints): + combined_constraints = " OR ".join([f"NOT event_name MATCHES {constraint}" for constraint in constraints]) + query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' + + query_request = requests.post( + self.signal_endpoint, + cookies=self.cookies, + headers=self.headers, + json={'query': query} + ) + return query_request.json()['data'][0][0] + + def get_total_cases(self): + count_request = requests.post( + self.signal_endpoint, + cookies=self.cookies, + headers=self.headers, + json={'query': 'SELECT COUNT(CASE_ID) FROM "defaultview-4"'}) + case_count = count_request.json()['data'][0][0] + return case_count + + def load_variants(self): + query_request = requests.post( + exp.signal_endpoint, + cookies=exp.cookies, + headers=exp.headers, + json={'query': 'SELECT Activity From "defaultview-4"'} + ) + data = query_request.json()['data'] + for activity in data: + self.event_log.add_trace(Trace(activity[0])) + + +def get_event_log(): + return f'Select * FROM "defaultview-4"' + +def determine_powerset(elements): + """Determines the powerset of a list of elements + Args: + elements (set): Set of elements + Returns: + list: Powerset of elements + """ + lset = list(elements) + ps_elements = chain.from_iterable( + combinations(lset, option) for option in range(len(lset) + 1) + ) + return [set(ps_element) for ps_element in ps_elements] exp = ExplainerSignal() -exp.add_constraint("(^'A')") -print(exp.constraints) \ No newline at end of file +exp.add_constraint("(^'review request')") +#exp.add_constraint("( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)") +exp.add_constraint("(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)") +exp.add_constraint("(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)") +print("Conf rate: " + str(exp.determine_conformance_rate())) +print("Fitness rate: " + str(exp.determine_fitness_rate())) + +query_request1 = requests.post( + exp.signal_endpoint, + cookies=exp.cookies, + headers=exp.headers, + json={'query': 'SELECT "Activity" From "defaultview-4"'} + ) +query_request = requests.post( + exp.signal_endpoint, + cookies=exp.cookies, + headers=exp.headers, + json={'query': 'SELECT Activity From "defaultview-4" WHERE "VARIANT-INDEX" = 2'} + ) + +data = query_request.json()['data'] +event_log = EventLog() + +first_ctrb = exp.constraint_ctrb_to_fitness(constraints=exp.constraints, index = 0) +snd_ctrb = exp.constraint_ctrb_to_fitness(constraints=exp.constraints, index = 1) +thr_ctrb = exp.constraint_ctrb_to_fitness(constraints=exp.constraints, index = 2) +print(f"First constraint contribution to fitness: {first_ctrb}") +print(f"Second constraint contribution to fitness: {snd_ctrb}") +print(f"third constraint contribution to fitness: {thr_ctrb}") +print(f"total distributon to fitness: {first_ctrb + snd_ctrb + thr_ctrb}") +first_ctrb = exp.constraint_ctrb_to_conformance(index = 0) +snd_ctrb = exp.constraint_ctrb_to_conformance(index = 1) +thr_ctrb = exp.constraint_ctrb_to_conformance(index = 2) + + +print(f"First constraint contribution to conf: {first_ctrb}") +print(f"Second constraint contribution to conf: {snd_ctrb}") +print(f"Third constraint contribution to conf: {thr_ctrb}") + +print(f"total distributon to conf loss: {first_ctrb + snd_ctrb + thr_ctrb}") diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/explainer/tutorial/explainer_tutorial_2.ipynb new file mode 100644 index 0000000..146bb34 --- /dev/null +++ b/explainer/tutorial/explainer_tutorial_2.ipynb @@ -0,0 +1,25 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Signal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 7a022e4..1746327 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -14,9 +14,7 @@ def test_remove_constraint(): explainer.add_constraint("A.*B.*C") explainer.add_constraint("B.*C") explainer.remove_constraint(0) - assert ( - "A.*B.*C" not in explainer.constraints, - ), "Constraint 'A.*B.*C' should be removed." + assert "A.*B.*C" not in explainer.constraints, "Constraint 'A.*B.*C' should be removed." # Test 3: Activation of constraints From de9ff739dac169924957981ba8359d7beb8deec0 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 10:50:12 +0200 Subject: [PATCH 35/54] Added cache to API calls --- explainer/explainer.py | 57 +- explainer/explainer_regex.py | 47 +- explainer/explainer_signal.py | 341 ++++++++++-- explainer/tutorial/diagram.json | 1 + explainer/tutorial/explainer_tutorial_2.ipynb | 501 +++++++++++++++++- tests/explainer/explainer_test.py | 4 +- 6 files changed, 847 insertions(+), 104 deletions(-) create mode 100644 explainer/tutorial/diagram.json diff --git a/explainer/explainer.py b/explainer/explainer.py index d21f91b..10904fa 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod - +from itertools import combinations class Explainer(ABC): def __init__(self): """ @@ -73,7 +73,7 @@ def counterfactual_expl(self, trace): pass @abstractmethod - def evaluate_similarity(self, trace): + def evaluate_similarity(self, trace, cmp_trace=None): # Implementation remains the same pass @@ -205,9 +205,17 @@ def get_most_frequent_variant(self): # Find the trace with the maximum count max_trace_tuple = max(self.log, key=self.log.get) - max_count = self.log[max_trace_tuple] - return Trace(list(max_trace_tuple)), max_count + return Trace(list(max_trace_tuple)) + def get_traces(self): + """ + Extracts and returns a list of all unique trace variants in the event log. + + :return: A list of Trace instances, each representing a unique trace. + """ + # Generate a Trace instance for each unique trace tuple in the log + return [Trace(list(trace_tuple)) for trace_tuple in self.log.keys()] + def __str__(self): """ Returns a string representation of the event log. @@ -228,3 +236,44 @@ def __iter__(self): for trace_tuple, count in sorted_log: for _ in range(count): yield Trace(list(trace_tuple)) + +def get_sublists(lst): + """ + Generates all possible non-empty sublists of a list. + + :param lst: The input list. + :return: A list of all non-empty sublists. + """ + sublists = [] + for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n + sublists.extend(combinations(lst, r)) + return sublists + +def levenshtein_distance(seq1, seq2): + """ + Calculates the Levenshtein distance between two sequences. + + Args: + seq1 (str): The first sequence. + seq2 (str): The second sequence. + + Returns: + int: The Levenshtein distance between the two sequences. + """ + size_x = len(seq1) + 1 + size_y = len(seq2) + 1 + matrix = [[0] * size_y for _ in range(size_x)] + for x in range(size_x): + matrix[x][0] = x + for y in range(size_y): + matrix[0][y] = y + + for x in range(1, size_x): + for y in range(1, size_y): + if seq1[x - 1] == seq2[y - 1]: + matrix[x][y] = matrix[x - 1][y - 1] + else: + matrix[x][y] = min( + matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 + ) + return matrix[size_x - 1][size_y - 1] \ No newline at end of file diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 2910e94..83b084c 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -1,7 +1,7 @@ import math import re from itertools import combinations, product, chain -from explainer import Explainer, Trace, EventLog +from explainer import Explainer, Trace, EventLog, get_sublists, levenshtein_distance class ExplainerRegex(Explainer): def __init__(self): @@ -397,7 +397,7 @@ def variant_ctrb_to_conformance_loss( :return: The contribution of the trace to the conformance loss as a float between 0 and 1. """ if not self.constraints and not constraints: - return "The explainer have no constraints" + return "The explainer have no constraints" if not constraints: constraints = self.constraints total_traces = len(event_log) @@ -469,19 +469,6 @@ def determine_powerset(elements): return [set(ps_element) for ps_element in ps_elements] -def get_sublists(lst): - """ - Generates all possible non-empty sublists of a list. - - :param lst: The input list. - :return: A list of all non-empty sublists. - """ - sublists = [] - for r in range(2, len(lst) + 1): # Generate combinations of length 2 to n - sublists.extend(combinations(lst, r)) - return sublists - - def get_iterative_subtrace(trace): """ Generates all possible non-empty contiguous sublists of a list, maintaining order. @@ -496,36 +483,6 @@ def get_iterative_subtrace(trace): return sublists - -def levenshtein_distance(seq1, seq2): - """ - Calculates the Levenshtein distance between two sequences. - - Args: - seq1 (str): The first sequence. - seq2 (str): The second sequence. - - Returns: - int: The Levenshtein distance between the two sequences. - """ - size_x = len(seq1) + 1 - size_y = len(seq2) + 1 - matrix = [[0] * size_y for _ in range(size_x)] - for x in range(size_x): - matrix[x][0] = x - for y in range(size_y): - matrix[0][y] = y - - for x in range(1, size_x): - for y in range(1, size_y): - if seq1[x - 1] == seq2[y - 1]: - matrix[x][y] = matrix[x - 1][y - 1] - else: - matrix[x][y] = min( - matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 - ) - return matrix[size_x - 1][size_y - 1] - exp = ExplainerRegex() traces = [ diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 2b66eae..0306bf0 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from explainer import Explainer, Trace, EventLog +from explainer import Explainer, Trace, EventLog, get_sublists, levenshtein_distance import re import math import SignavioAuthenticator @@ -7,6 +7,26 @@ from itertools import combinations, chain import requests from conf import system_instance, workspace_id, user_name, pw + +KEYWORDS = [ + "ABS", "ALL", "ANALYZE", "AND", "ANY", "AS", "ASC", "AVG", "BARRIER", "BEHAVIOUR", "BETWEEN", "BOOL_AND", + "BOOL_OR", "BUCKET", "BY", "CASE", "CASE_ID", "CATEGORY", "CEIL", "CHAR_INDEX", "CHAR_LENGTH", "COALESCE", + "CONCAT", "COUNT", "CREATE", "CURRENT", "DATE_ADD", "DATE_DIFF", "DATE_PART", "DATE_TRUNC", "DEFAULT", + "DENSE_RANK", "DESC", "DESCRIBE", "DISTINCT", "DROP", "DURATION", "DURATION_BETWEEN", "DURATION_FROM_DAYS", + "DURATION_FROM_MILLISECONDS", "DURATION_TO_DAYS", "DURATION_TO_MILLISECONDS", "ELSE", "END", "END_TIME", + "EVENT_ID", "EVENT_NAME", "EVENTS", "EXACT", "EXPLAIN", "EXTERNAL", "FALSE", "FILL", "FILTER", "FIRST", + "FLATTEN", "FLOOR", "FOLLOWING", "FORMAT", "FROM", "GRANT", "GROUP", "HAVING", "IF", "ILIKE", "IN", "INVOKER", + "IS", "JOIN", "JSON", "LAG", "LAST", "LEAD", "LEFT", "LIKE", "LIMIT", "LOCATION", "LOG", "MATCHES", "MAX", + "MEDIAN", "MIN", "NOT", "NOW", "NULL", "NULLS", "OCCURRENCE", "ODATA", "OFFSET", "ON", "ONLY", "OR", "ORDER", + "OUTER", "OVER", "PARQUET", "PARTITION", "PERCENT", "PERCENTILE_CONT", "PERCENTILE_DESC", "PERMISSIONS", "POW", + "PRECEDING", "PRIVATE", "PUBLIC", "RANGE", "RANK", "REGR_INTERCEPT", "REGR_SLOPE", "REPEATABLE", "REPLACE", + "RIGHT", "ROUND", "ROW", "ROW_NUMBER", "ROWS", "SECURITY", "SELECT", "SIGN", "SQRT", "START_TIME", "STDDEV", + "SUBSTRING", "SUBSTRING_AFTER", "SUBSTRING_BEFORE", "SUM", "TABLE", "TABULAR", "TEXT", "THEN", "TIMESERIES", + "TIMESTAMP", "TO", "TO_NUMBER", "TO_STRING", "TO_TIMESTAMP", "TRUE", "TRUNC", "UNBOUNDED", "UNION", "USING", + "VIEW", "WHEN", "WHERE", "WITH", "WITHIN", "" +] + + class ExplainerSignal(Explainer): def __init__(self): super().__init__() @@ -14,11 +34,15 @@ def __init__(self): self.auth_data = self.authenticator.authenticate() self.cookies = {'JSESSIONID': self.auth_data['jsesssion_ID'], 'LBROUTEID': self.auth_data['lb_route_ID']} self.headers = {'Accept': 'application/json', 'x-signavio-id': self.auth_data['auth_token']} - self.signal_endpoint = system_instance + '/g/api/pi-graphql/signal' self.event_log = EventLog() - self.load_variants() - self.minimal_expl(self.event_log.get_most_frequent_variant()) + self.signal_endpoint = None + self.cache = {} + + def set_endpoint(self, endpoint= '/g/api/pi-graphql/signal'): + self.signal_endpoint = system_instance + endpoint + self.load_variants() + def remove_constraint(self, idx): """ Removes a constraint by index and updates the nodes list if necessary. @@ -47,22 +71,186 @@ def activation(self, trace, constraints=None): def conformant(self, trace, constraints=None): if not constraints: constraints = self.constraints + if len(constraints) > 1: + constraints = " AND ".join([f"event_name MATCHES {constraint}" for constraint in constraints]) + else: + constraints = "".join(f"event_name MATCHES {constraints[0]}") + query = f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' + conformant = False + cache_key = hash(query) + if cache_key in self.cache: + for res in self.cache[cache_key]: + if trace.nodes == res[0]: + conformant = True + break + return conformant + + query_request = requests.post( + self.signal_endpoint, + cookies=self.cookies, + headers=self.headers, + json={'query': query} + ) + result = query_request.json()['data'] + self.cache[cache_key] = result # Store the result in cache + + for res in result: + if trace.nodes == res[0]: + conformant = True + break + return conformant - pass def contradiction(self, check_multiple=False, max_length=10): pass def minimal_expl(self, trace): - print(trace) - + + if self.conformant(trace): + return "The trace is already conformant, no changes needed." + explanations = None + + for constraint in self.constraints: + for subtrace in get_sublists(trace): + trace_str = "".join(subtrace) + if not re.search(constraint, trace_str): + explanations = ( + f"Constraint ({constraint}) is violated by subtrace: {subtrace}" + ) + break + + if explanations: + return "Non-conformance due to: " + explanations + else: + return "Trace is non-conformant, but the specific constraint violation could not be determined." + def counterfactual_expl(self, trace): - pass + constraints = self.constraints + if len(constraints) > 1: + constraints = " AND ".join([f"event_name MATCHES {constraint}" for constraint in constraints]) + else: + constraints = "".join(f"event_name MATCHES {constraints[0]}") + query = f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' + query_request = requests.post( + self.signal_endpoint, + cookies=self.cookies, + headers=self.headers, + json={'query': query} + ) + result = query_request.json()['data'] + + best_score = -float("inf") + for res in result: + current_score = self.evaluate_similarity(trace, "".join(res[0])) + if current_score > best_score: + best_score = current_score + self.adherent_trace = Trace(res[0]) + + return self.operate_on_trace(trace, 0, "") + + def counter_factual_helper(self, working_trace, explanation, depth=0): + if self.conformant(working_trace): + return f"{explanation}" + if depth > 100: + return f"{explanation}\n Maximum depth of {depth -1} reached" + return self.operate_on_trace(working_trace, 0, explanation, depth) + + def operate_on_trace(self, trace, score, explanation_path, depth=0): + explanation = None + counter_factuals = self.modify_subtrace(trace) + best_subtrace = None + best_score = -float("inf") + for subtrace in counter_factuals: + current_score = self.evaluate_similarity(subtrace[0].nodes) + if current_score > best_score: + best_score = current_score + best_subtrace = subtrace[0] + explanation = subtrace[1] + if best_subtrace == None: + for subtrace in counter_factuals: + self.operate_on_trace(subtrace[0], score, explanation_path, depth + 1) + explanation_string = explanation_path + "\n" + str(explanation) + return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) + + + def modify_subtrace(self, trace): + """ + Modifies the given trace to meet constraints by adding nodes where the pattern fails. + + Parameters: + - trace: A list of node identifiers + + Returns: + - A list of potential subtraces each modified to meet constraints. + """ + potential_subtraces = [] + possible_additions = self.get_nodes_from_constraint() + for i, s_trace in enumerate(get_iterative_subtrace(trace)): + for con in self.constraints: + new_trace_str = "".join(s_trace) + match = re.search(con, new_trace_str) + if not match: + for add in possible_additions: + potential_subtraces.append( + [ + Trace(s_trace + [add] + trace.nodes[i + 1 :]), + f"Addition (Added {add} at position {i+1}): " + + "->".join(s_trace + [add] + trace.nodes[i + 1 :]), + ] + ) + potential_subtraces.append( + [ + Trace(s_trace[:-1] + [add] + trace.nodes[i:]), + f"Addition (Added {add} at position {i}): " + + "->".join(s_trace[:-1] + [add] + trace.nodes[i:]), + ] + ) + + potential_subtraces.append( + [ + Trace(s_trace[:-1] + trace.nodes[i + 1 :]), + f"Subtraction (Removed {s_trace[i]} from position {i}): " + + "->".join(s_trace[:-1] + trace.nodes[i + 1 :]), + ] + ) + return potential_subtraces + def get_nodes_from_constraint(self, constraint=None): + """ + Extracts unique nodes from a constraint pattern. + + :param constraint: The constraint pattern as a string. + :return: A list of unique nodes found within the constraint. + """ + if constraint is None: + all_nodes = set() + for con in self.constraints: + all_nodes.update(self.filter_keywords(con)) + return list(set(all_nodes)) + else: + return list(set(self.filter_keywords(constraint))) - def determine_shapley_value(self, log, constraints, index): - pass + def filter_keywords(self, text): + text = re.sub(r'\s+', '_', text.strip()) + words = re.findall(r"\b[A-Z_a-z]+\b", text) + modified_words = [word.replace("_", " ") for word in words] + filtered_words = [word for word in modified_words if word.strip() not in KEYWORDS] + + return filtered_words - def evaluate_similarity(self, trace): - pass + def evaluate_similarity(self, trace, cmp_trace=None): + """ + Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance. + + :param trace: The trace to compare with the adherent trace. + :return: A normalized score indicating the similarity between the adherent trace and the given trace. + """ + if cmp_trace == None: + cmp_trace = self.adherent_trace.nodes + trace_len = len("".join(trace)) + length = len(cmp_trace) + lev_distance = levenshtein_distance(cmp_trace, "".join(trace)) + max_distance = max(length, trace_len) + normalized_score = 1 - lev_distance / max_distance + return normalized_score def determine_conformance_rate(self, event_log = None, constraints=None): if constraints == None: @@ -89,12 +277,32 @@ def determine_fitness_rate(self, event_log = None, constraints = None): def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): - # Implementation remains the same - pass + if not self.constraints and not constraints: + return "The explainer have no constraints" + if not constraints: + constraints = self.constraints + total_traces = len(event_log) + contribution_of_trace = 0 + if not self.conformant(trace, constraints= constraints): + contribution_of_trace = event_log.get_variant_count(trace) + + return contribution_of_trace / total_traces def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): - # Implementation remains the same - pass + if not self.constraints and not constraints: + return "The explainer have no constraints" + if not constraints: + constraints = self.constraints + total_traces = len(event_log) + contribution_of_trace = 0 + for con in constraints: + if self.conformant(trace, constraints=[con]): + contribution_of_trace += 1 + nr = event_log.get_variant_count(trace) + contribution_of_trace = contribution_of_trace / len(constraints) + contribution_of_trace = nr * contribution_of_trace + return contribution_of_trace / total_traces + def constraint_ctrb_to_conformance(self, log = None, constraints = None, index = -1): """Determines the Shapley value-based contribution of a constraint to a the @@ -147,52 +355,57 @@ def constraint_ctrb_to_fitness(self, log = None, constraints = None, index = -1) return ctrb_count / (len_log * len(constraints)) def check_conformance(self, constraint): - query_request = requests.post( - self.signal_endpoint, - cookies=self.cookies, - headers=self.headers, - json={'query': f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}'}) - return query_request.json()['data'][0][0] - - def check_conformances(self, constraints): - combined_constraints = " AND ".join([f"event_name MATCHES {constraint}" for constraint in constraints]) - query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' - + query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}' + cache_key = hash(query) + if cache_key in self.cache: + return self.cache[cache_key] query_request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query} - ) - return query_request.json()['data'][0][0] + json={'query': query}) + result = query_request.json()['data'][0][0] + self.cache[cache_key] = result + return result def check_violations(self, constraints): combined_constraints = " OR ".join([f"NOT event_name MATCHES {constraint}" for constraint in constraints]) query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' - + cache_key = hash(query) + if cache_key in self.cache: + return self.cache[cache_key] query_request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, json={'query': query} ) - return query_request.json()['data'][0][0] + result = query_request.json()['data'][0][0] + self.cache[cache_key] = result + return result def get_total_cases(self): + query = 'SELECT COUNT(CASE_ID) FROM "defaultview-4"' + cache_key = hash(query) + if cache_key in self.cache: + return self.cache[cache_key] count_request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': 'SELECT COUNT(CASE_ID) FROM "defaultview-4"'}) + json={'query': query}) case_count = count_request.json()['data'][0][0] + self.cache[cache_key] = case_count return case_count def load_variants(self): + query = 'SELECT Activity From "defaultview-4"' + query_request = requests.post( - exp.signal_endpoint, - cookies=exp.cookies, - headers=exp.headers, - json={'query': 'SELECT Activity From "defaultview-4"'} + self.signal_endpoint, + cookies=self.cookies, + headers=self.headers, + json={'query': query} ) data = query_request.json()['data'] for activity in data: @@ -215,29 +428,52 @@ def determine_powerset(elements): ) return [set(ps_element) for ps_element in ps_elements] +def get_iterative_subtrace(trace): + """ + Generates all possible non-empty contiguous sublists of a list, maintaining order. + + :param lst: The input list. + n: the minmum length of sublists + :return: A list of all non-empty contiguous sublists. + """ + sublists = [] + for i in range(0, len(trace)): + sublists.append(trace.nodes[0 : i + 1]) + return sublists +""" exp = ExplainerSignal() -exp.add_constraint("(^'review request')") -#exp.add_constraint("( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)") +exp.set_endpoint() +exp.add_constraint("(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)") exp.add_constraint("(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)") exp.add_constraint("(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)") +trace = Trace(['credit requested', 'review request', 'prepare special terms', 'assess risks', 'prepare special terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']) + +#print(exp.minimal_expl(trace)) +#print(exp.counterfactual_expl(trace)) + print("Conf rate: " + str(exp.determine_conformance_rate())) print("Fitness rate: " + str(exp.determine_fitness_rate())) -query_request1 = requests.post( - exp.signal_endpoint, - cookies=exp.cookies, - headers=exp.headers, - json={'query': 'SELECT "Activity" From "defaultview-4"'} - ) -query_request = requests.post( - exp.signal_endpoint, - cookies=exp.cookies, - headers=exp.headers, - json={'query': 'SELECT Activity From "defaultview-4" WHERE "VARIANT-INDEX" = 2'} - ) +total_distribution = 0 +for trace in exp.event_log.get_traces(): + ctrb = exp.variant_ctrb_to_fitness( + event_log=exp.event_log, + trace=trace, + ) + total_distribution += ctrb +print(f"Total distribution to the fitness rate is {total_distribution}") + + +total_distribution = 0 +for trace in exp.event_log.get_traces(): + ctrb = exp.variant_ctrb_to_conformance_loss( + event_log=exp.event_log, + trace=trace, + ) + total_distribution += ctrb -data = query_request.json()['data'] +print(f"Total distribution to the conformance loss is {total_distribution}") event_log = EventLog() first_ctrb = exp.constraint_ctrb_to_fitness(constraints=exp.constraints, index = 0) @@ -257,3 +493,4 @@ def determine_powerset(elements): print(f"Third constraint contribution to conf: {thr_ctrb}") print(f"total distributon to conf loss: {first_ctrb + snd_ctrb + thr_ctrb}") +""" \ No newline at end of file diff --git a/explainer/tutorial/diagram.json b/explainer/tutorial/diagram.json new file mode 100644 index 0000000..d3811b0 --- /dev/null +++ b/explainer/tutorial/diagram.json @@ -0,0 +1 @@ +{"resourceId": "canvas", "formats": {}, "ssextensions": [], "bounds": {"upperLeft": {"x": 0, "y": 0}, "lowerRight": {"x": 1485, "y": 1050}}, "stencilset": {"namespace": "http://b3mn.org/stencilset/bpmn2.0#", "url": "/stencilsets/bpmn2.0/bpmn2.0.json?version=16.16.0"}, "defaultFormats": [{"backgroundColor": "#FFFFFF", "flat": true, "roles": ["Task"]}], "language": "en_us", "stencil": {"id": "BPMNDiagram"}, "properties": {"meta-solutionprocessflows": [], "meta-countryregionrelevance": "", "meta-vertraulichkeit_de_de": "Nur f\u00fcr den internen Gebrauch", "pmcontact": "", "processtype": "None", "auditing": "", "meta-vertraulichkeit_fr_fr": "Nur f\u00fcr den internen Gebrauch", "objective": "", "meta-valuedriver": [], "processowner": "timotheus.kampik@signavio.com", "meta-mcdorganization": "", "processgoal": "", "meta-modularprocess": "", "meta-clouddeliveryprocesschanged": "", "ikskontrolle": [], "meta-efihierarchykey": "", "prozessanalystin": [], "monitoring": "", "version": "", "prozesskategorie": "", "certificationstandards": "", "generalplannedprocessmaturitylev": "", "prozessanalyst": "", "meta-clouddeliveryprocesschanger": [], "dataoutputs": "", "meta-clouddeliveryprocesschangev": [], "meta-usedin": [], "meta-inframework": "", "meta-geltungsbereich": [], "meta-m2detailedprocessdocumentat": [], "isexecutable": "", "expressionlanguage": "http://www.w3.org/1999/XPath", "palevel": "", "prozessinput": "", "exporter": "", "meta-emailadresse": "@vr-finanzdienstleistung.de", "prozessscope": "", "meta-m3annualimprovementtargetsn": "", "meta-m2processcontinuityplan": [], "meta-processparticipant2": "", "meta-processparticipant1": "", "meta-businessunit": "", "meta-verb": "", "meta-maturityassessment": "", "meta-m1knowledgetransfertext": "", "clriskid": "", "m2processvariants": [], "meta-setupguide": "", "topgoalppi2014": "", "meta-bpxtodel": "", "exporterversion": "", "executable": "", "slbespokefla": "", "meta-riskcontrol": "", "meta-scopeitemversion": [], "meta-mcdprocessslasandkpis": "", "meta-vorgangsvorlage": [], "fitfunction": [], "meta-executioncoststotal": "", "meta-processowner": "", "meta-maturityassessmentdetailedv": "", "language": "English", "meta-kundedesoutputs2": [], "meta-clriskid": "N/A", "sflevel": "", "meta-businessdeliverables": "", "meta-clqrqid": "N/A", "meta-m1knowledgetransfertext2": "", "meta-ml": [], "generalactualprocessmaturityleve": "", "meta-m3singletaskingnew": "", "sfsme": "", "slbespokeflag": "No", "meta-clouddeliveryprocesscha": "", "processmanagernew": [], "meta-standardindividual": "", "generalprocessoccurrence": "", "meta-endtoendscenario": "", "externaldocuments": "", "businessunit": "", "signals": "", "meta-m3onlinerealtimeprocessdata": "", "prozessrelevantedokumente": [], "exchange": "", "meta-prozessreifegrad": "", "meta-mcdprocessprimaryresponsibl": "", "meta-tasktutorials": "", "meta-m3processstandardizationnew": "", "prozessoutput": "", "meta-emailadressefg": "@vr-finanzdienstleistung.de", "meta-vertraulichkeit": "Nur f\u00fcr den internen Gebrauch", "isclosed": "", "legalentity": "", "inputsets": "", "m2detailedprocessdocumentation": [], "meta-mynumber": "", "interfaces": "", "m3processvisionlongtermneu": "", "meta-m3continuousimprovementproc": "", "meta-arissapid": "", "outputsets": "", "meta-kpmanager": "", "generalexternalprocessdocumentat": [], "meta-bestpractices": [], "sfmodeller": "", "clqrqid": "", "meta-emailadresseautor": "@vr-finanzdienstleistung.de", "meta-lieferantdesinputs": [], "meta-m2processperformancecockpit": "", "clcontrolid": "", "meta-product": "", "meta-modelingstatus": "Draft", "iso9000ff": "", "typelanguage": "http://www.w3.org/2001/XMLSchema", "processslas": "", "meta-processgoal": "", "meta-mcdlinkofpublishedprocess": "", "businessownernew": [], "processlinks": [], "meta-cycsemanticvalidation": "", "meta-arissaptype": "", "generalprocessstatus": "InProcess", "meta-generalrelateddiagrams": [], "sfregion": "", "meta-generalexternalprocessdocum": [], "effectifprozessverantwortlicherb": "", "messages": "", "meta-deliverymodel": "", "anforderungen": [], "topgoall3process2014": "", "meta-mcdservicearea": "", "datainputs": "", "meta-idwpsrelevant": "", "meta-sapstandardcontent": [], "meta-mcdoperationalprocessid": "", "meta-prozessverantwortlicher2": "", "meta-sfregion": "", "meta-eingangswege": [], "datainputset": "", "flat": "", "meta-mcdprocesschecklist": "", "bpmn2_imports": "", "categories": "", "prozessverantwortlicher": [], "effectifprozessverantwortlicher": "", "meta-vertraulichkeit_es_es": "Nur f\u00fcr den internen Gebrauch", "meta-generalquarterendclosingqec": "", "meta-m2processcontinuitystatus": "", "normen": [], "meta-lastreviewedon": "", "meta-efiideasupport": "", "meta-anweisungenimvorgang": "", "meta-zertifizierung": [], "meta-itsystems": [], "meta-testscript2": "", "meta-setupusingcloudintegrationa": "", "meta-maturitylevel1": "", "meta-lastreviewcommentm": "", "meta-processstatus2": "", "processid": "", "meta-vertraulichkeit_it_it": "Nur f\u00fcr den internen Gebrauch", "meta-itsysteme": [], "meta-prozesseigner": "", "creationdate": "", "meta-m3output": "", "targetnamespace": "http://www.signavio.com/bpmn20", "meta-itapplications": [], "meta-generalprocessnumber": "", "meta-kunde": [], "meta-keyprocessflows": "", "author": "", "market": "", "meta-teilprozesseigner": "", "meta-prozesskategorie": "", "meta-overview": "", "meta-flligam": "", "properties2": "", "meta-legalentity2": "", "meta-accessmaturityassessmenther": "https://workflow.signavio.com/sapprocessmapworkspacesimonblattmann/cases/start/610b89d4017af21cafa24b87", "errors": "", "namespaces": "", "meta-efikeywordsandtags": "", "meta-prozesseinordnung": "", "m2processriskquestionaire": [], "m1processonepagerneu": [], "meta-clcontrolid": "N/A", "bapol3responsiblenew": "", "meta-kennzahlen": [], "meta-vertraulichkeit_nl_nl": "Nur f\u00fcr den internen Gebrauch", "meta-businessbenefits": "", "meta-gltigkeitasdauer": "", "prozessreifegrad": "", "sfopsbu": "", "meta-externaldocuments4": [], "meta-artikelnr": [], "orientation": "horizontal", "sfreviewer": "", "resources": "", "prozessstufe": "", "modificationdate": "", "meta-sh": [], "dataoutputset": "", "itemdefinitions": "", "properties": ""}, "childShapes": [{"outgoing": [{"resourceId": "sid-85147FD1-D065-4B20-9AB9-DC7A04D23C48"}], "resourceId": "sid-38B9A31A-F0F5-4CC6-9215-BEBE7737BC4A", "formats": {}, "dockers": [{"x": 15, "y": 15}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-85147FD1-D065-4B20-9AB9-DC7A04D23C48"}, "bounds": {"upperLeft": {"x": 210.609375, "y": 248}, "lowerRight": {"x": 254.15625, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-7792BFD6-00E6-4610-9FB6-BC7DB3A82EB6"}], "resourceId": "sid-DE01AA8F-B36A-4796-9597-651C8B46C6F3", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-7792BFD6-00E6-4610-9FB6-BC7DB3A82EB6"}, "bounds": {"upperLeft": {"x": 355.2421737945171, "y": 248.26304803033779}, "lowerRight": {"x": 380.1015762054829, "y": 248.39320196966221}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-26C88D1A-96C0-44BD-9BAF-7D25C707388B"}], "resourceId": "sid-25136035-D01E-4DC5-AAF9-FE3F803B865B", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-26C88D1A-96C0-44BD-9BAF-7D25C707388B"}, "bounds": {"upperLeft": {"x": 420.796875, "y": 248.5}, "lowerRight": {"x": 444.1875, "y": 248.5}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-56E77E6D-0667-43B3-855B-AEA6BD2AF93E"}], "resourceId": "sid-088B345E-B2CA-455B-A649-B3E8A3C9157E", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 400.5, "y": 466}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-56E77E6D-0667-43B3-855B-AEA6BD2AF93E"}, "bounds": {"upperLeft": {"x": 400.5, "y": 268.19140625}, "lowerRight": {"x": 464.80078125, "y": 466}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}], "resourceId": "sid-224B5102-FEB8-44B3-9403-387F263E41A5", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 20, "y": 20}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}, "bounds": {"upperLeft": {"x": 663.4140625, "y": 248}, "lowerRight": {"x": 708.1328125, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}], "resourceId": "sid-BE5261E4-44C3-4C0E-A6A9-026989C331D1", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 728.5, "y": 362}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}, "bounds": {"upperLeft": {"x": 663.62890625, "y": 268.12109375}, "lowerRight": {"x": 728.5, "y": 362}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": false, "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}], "resourceId": "sid-CA7B85EC-6945-4F46-B7BB-F8208FD568E0", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}, "bounds": {"upperLeft": {"x": 896.6288968799925, "y": 248.2191727137662}, "lowerRight": {"x": 940.6484468700075, "y": 248.4097335362338}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-EA2F6C72-7BD0-4726-84FD-9045BE44B73C"}], "resourceId": "sid-77ADCCB1-8CE0-48B2-BA9C-871C9CBB8214", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-EA2F6C72-7BD0-4726-84FD-9045BE44B73C"}, "bounds": {"upperLeft": {"x": 981.6952985029254, "y": 248.26700968123242}, "lowerRight": {"x": 1005.5351702470746, "y": 248.39314656876758}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-850D153A-D4C2-4853-B59A-13555E19D1A3"}], "resourceId": "sid-B7ED9CDB-A923-458F-AA47-A5E7812477BE", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 14, "y": 14}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-850D153A-D4C2-4853-B59A-13555E19D1A3"}, "bounds": {"upperLeft": {"x": 1106.3671875, "y": 248}, "lowerRight": {"x": 1129.3984375, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}], "resourceId": "sid-DB57838D-7E05-4F45-B36E-765F6F074084", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 961.5, "y": 466}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}, "bounds": {"upperLeft": {"x": 565.7080078125, "y": 268.19140625}, "lowerRight": {"x": 961.5, "y": 466}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-86ACDE48-0EBD-40FC-B7D0-C4E9D447E4F8"}], "resourceId": "sid-FA8EA2F2-B2E7-46FC-9B60-441B02FDF8F8", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 465.5, "y": 362}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-86ACDE48-0EBD-40FC-B7D0-C4E9D447E4F8"}, "bounds": {"upperLeft": {"x": 465.5, "y": 268.12109375}, "lowerRight": {"x": 562.44921875, "y": 362}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"probability": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "bordercolor": "#000000", "name_de_de": "nein", "transportzeiten": "", "flat": "", "name": "no", "showdiamondmarker": ""}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-DE01AA8F-B36A-4796-9597-651C8B46C6F3"}], "resourceId": "sid-85147FD1-D065-4B20-9AB9-DC7A04D23C48", "formats": {}, "bounds": {"upperLeft": {"x": 255, "y": 208}, "lowerRight": {"x": 355, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Antrag \u00fcberpr\u00fcfen", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Review \nrequest", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-25136035-D01E-4DC5-AAF9-FE3F803B865B"}, {"resourceId": "sid-088B345E-B2CA-455B-A649-B3E8A3C9157E"}], "resourceId": "sid-7792BFD6-00E6-4610-9FB6-BC7DB3A82EB6", "formats": {}, "bounds": {"upperLeft": {"x": 380, "y": 228}, "lowerRight": {"x": 420, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "ParallelGateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "type": "http://b3mn.org/stencilset/bpmn2.0#Exclusive_Databased_Gateway", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "gatewaytype": "AND", "synchronousexecutionjbpm": "", "gate_assignments": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "processid": "", "gates_outgoingsequenceflow": "", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-FA8EA2F2-B2E7-46FC-9B60-441B02FDF8F8"}, {"resourceId": "sid-2C0CB6FB-7B15-4570-975B-89EBCF4072F4"}], "resourceId": "sid-26C88D1A-96C0-44BD-9BAF-7D25C707388B", "formats": {}, "bounds": {"upperLeft": {"x": 445, "y": 228}, "lowerRight": {"x": 485, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Exclusive_Databased_Gateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "adapterconfiguration": "", "gatewaytype": "XOR", "synchronousexecutionjbpm": "", "gate_assignments": "", "question": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "name_de_de": "Standard-\nkonditionen \nanwendbar?", "processid": "", "gates_outgoingsequenceflow": "", "name": "Standard terms\napplicable?", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "x": 68, "y": -24, "valign": "middle", "styles": {"size": "18.0"}, "align": "center"}]}, {"outgoing": [{"resourceId": "sid-224B5102-FEB8-44B3-9403-387F263E41A5"}], "resourceId": "sid-F858CA2F-4719-435E-9FF9-8C906CADF2F9", "formats": {}, "bounds": {"upperLeft": {"x": 563, "y": 208}, "lowerRight": {"x": 663, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Konditionen berechnen", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Calculate \nterms", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-CA7B85EC-6945-4F46-B7BB-F8208FD568E0"}], "resourceId": "sid-49FD1952-AF99-4103-963B-22CB8F6C0BB6", "formats": {}, "bounds": {"upperLeft": {"x": 796, "y": 208}, "lowerRight": {"x": 896, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Vertrag vorbereiten", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Prepare contract", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-77ADCCB1-8CE0-48B2-BA9C-871C9CBB8214"}], "resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31", "formats": {}, "bounds": {"upperLeft": {"x": 941, "y": 228}, "lowerRight": {"x": 981, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "ParallelGateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "type": "http://b3mn.org/stencilset/bpmn2.0#Exclusive_Databased_Gateway", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "gatewaytype": "AND", "synchronousexecutionjbpm": "", "gate_assignments": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "processid": "", "gates_outgoingsequenceflow": "", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-B7ED9CDB-A923-458F-AA47-A5E7812477BE"}], "resourceId": "sid-EA2F6C72-7BD0-4726-84FD-9045BE44B73C", "formats": {}, "bounds": {"upperLeft": {"x": 1006, "y": 208}, "lowerRight": {"x": 1106, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Angebot \nsenden", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Send quote", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [], "resourceId": "sid-850D153A-D4C2-4853-B59A-13555E19D1A3", "formats": {}, "bounds": {"upperLeft": {"x": 1131, "y": 234}, "lowerRight": {"x": 1159, "y": 262}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "EndNoneEvent"}, "dockers": [], "properties": {"datainputs": "", "meta-nachfolgerprozesse": [], "datainput": "", "datainputassociations": "", "auditing": "", "trigger": "None", "monitoring": "", "meta-succeedingprocesses": [], "followingprocesses": [], "bordercolor": "#000000", "datainputassociations_throwevents": "", "folgeprozess": "", "bgcolor": "#ffffff", "name_de_de": "Angebot \ngesendet", "inputset": "", "flat": "", "inputsets": "", "name": "Quote\nsent", "properties2": "", "properties": "", "nachfolgerprozesse": []}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-38B9A31A-F0F5-4CC6-9215-BEBE7737BC4A"}], "resourceId": "sid-1A2C3AFC-4775-461F-9316-ED40DD0B4CA8", "formats": {}, "bounds": {"upperLeft": {"x": 180, "y": 233}, "lowerRight": {"x": 210, "y": 263}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "StartNoneEvent"}, "dockers": [], "properties": {"vorgngerprozesse": [], "meta-preceedingprocesses": [], "preceedingprocesses": [], "auditing": "", "type": "http://b3mn.org/stencilset/bpmn2.0#StartMessageEvent", "frequency": "", "flat": "", "links": [], "meta-vorgngerprozesse": [], "applyincalc": true, "dataoutput": "", "dataoutputassociations": "", "dataoutputassociations_catchevents": "", "trigger": "None", "monitoring": "", "bordercolor": "#000000", "outputset": "", "outputsets": "", "bgcolor": "#ffffff", "name_de_de": "Kredit-Antrag", "processid": "", "name": "Credit\nrequested", "dataoutputs": "", "properties2": "", "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-BE5261E4-44C3-4C0E-A6A9-026989C331D1"}], "resourceId": "sid-86ACDE48-0EBD-40FC-B7D0-C4E9D447E4F8", "formats": {}, "bounds": {"upperLeft": {"x": 563, "y": 322}, "lowerRight": {"x": 663, "y": 402}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Sonder-konditionen vorbereiten", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Prepare special terms", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-DB57838D-7E05-4F45-B36E-765F6F074084"}], "resourceId": "sid-56E77E6D-0667-43B3-855B-AEA6BD2AF93E", "formats": {}, "bounds": {"upperLeft": {"x": 465, "y": 426}, "lowerRight": {"x": 565, "y": 506}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Risiken bewerten", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Assess risks", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-F858CA2F-4719-435E-9FF9-8C906CADF2F9"}], "resourceId": "sid-2C0CB6FB-7B15-4570-975B-89EBCF4072F4", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "orientation": "lr", "distance": 6.316500663757324, "x": 523.6798001733436, "y": 248.30278033839545, "from": 0, "valign": "bottom", "styles": {"size": "18.0"}, "to": 1, "align": "right"}], "target": {"resourceId": "sid-F858CA2F-4719-435E-9FF9-8C906CADF2F9"}, "bounds": {"upperLeft": {"x": 484.93749425457406, "y": 248.17135856103246}, "lowerRight": {"x": 562.4492244954259, "y": 248.43411018896754}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"probability": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "bordercolor": "#000000", "name_de_de": "ja", "transportzeiten": "", "flat": "", "name": "yes", "showdiamondmarker": ""}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-04C1509D-573F-4F83-898D-FAD6E7B606DA"}], "resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E", "formats": {}, "bounds": {"upperLeft": {"x": 708, "y": 228}, "lowerRight": {"x": 748, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Exclusive_Databased_Gateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "adapterconfiguration": "", "gatewaytype": "XOR", "synchronousexecutionjbpm": "", "gate_assignments": "", "question": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "processid": "", "gates_outgoingsequenceflow": "", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "x": 68, "y": -25, "valign": "middle", "styles": {"size": "18.0"}, "align": "center"}]}, {"outgoing": [{"resourceId": "sid-49FD1952-AF99-4103-963B-22CB8F6C0BB6"}], "resourceId": "sid-04C1509D-573F-4F83-898D-FAD6E7B606DA", "formats": {}, "dockers": [{"x": 20, "y": 20}, {"x": 50, "y": 40}], "labels": [], "target": {"resourceId": "sid-49FD1952-AF99-4103-963B-22CB8F6C0BB6"}, "bounds": {"upperLeft": {"x": 748.359375, "y": 248}, "lowerRight": {"x": 795.21875, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": false, "bordercolor": "#000000"}, "childShapes": []}]} \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/explainer/tutorial/explainer_tutorial_2.ipynb index 146bb34..ee057a0 100644 --- a/explainer/tutorial/explainer_tutorial_2.ipynb +++ b/explainer/tutorial/explainer_tutorial_2.ipynb @@ -7,17 +7,514 @@ "## Signal" ] }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('../')\n", + "from explainer import Explainer, Trace, EventLog\n", + "from explainer_signal import ExplainerSignal" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Contribution functions\n", + "\n", + "We'll start with conformance rate and fitness rate, given some arbritary constraints" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conformance rate: 0.71\n", + "Fitness rate : 0.8933333333333333\n" + ] + } + ], + "source": [ + "exp = ExplainerSignal()\n", + "exp.set_endpoint('/g/api/pi-graphql/signal') #Load the data set\n", + "\n", + "exp.add_constraint(\"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\")\n", + "exp.add_constraint(\"(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\")\n", + "exp.add_constraint(\"(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\")\n", + "\n", + "conf_rate = exp.determine_conformance_rate()\n", + "fit_rate = exp.determine_fitness_rate()\n", + "print(\"Conformance rate: \" + str(conf_rate))\n", + "print(\"Fitness rate : \" + str(fit_rate))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_conformance_loss` determines how much a specific variant contributes to the overall conformance loss.\n", + "\n", + "This is done slightly different when working with a data set, because all variations have to be considered" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution is: 0.0, for trace ['review request', 'calculate terms', 'credit requested', 'calculate terms', 'assess risks', 'prepare contract', 'review request', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.0, for trace ['credit requested', 'review request', 'review request', 'prepare contract', 'assess risks', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.0, for trace ['credit requested', 'calculate terms', 'review request', 'review request', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.0, for trace ['credit requested', 'review request', 'review request', 'assess risks', 'assess risks', 'calculate terms', 'prepare contract', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.01, for trace ['send quote', 'credit requested', 'review request', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'review request', 'quote sent']\n", + "Contribution is: 0.0, for trace ['credit requested', 'review request', 'review request', 'calculate terms', 'calculate terms', 'assess risks', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Total distribution to the conformance loss is 0.2900000000000001\n", + "Our total conformance loss is 0.29000000000000004\n" + ] + } + ], + "source": [ + "total_distribution = 0\n", + "i = 0\n", + "for trace in exp.event_log.get_traces():\n", + " i +=1\n", + " ctrb = exp.variant_ctrb_to_conformance_loss(\n", + " event_log=exp.event_log,\n", + " trace=trace,\n", + " )\n", + " total_distribution += ctrb\n", + " # Let's just show some traces\n", + " if i % 10 == 0:\n", + " print(f\"Contribution is: {ctrb}, for trace {trace.nodes}\")\n", + "print(f\"Total distribution to the conformance loss is {total_distribution}\")\n", + "print(f\"Our total conformance loss is {1 - conf_rate}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_fitness` determines how much a specific variant contributes to the overall fitness rate.\n", + "\n", + "This is also done in the same way" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution is: 0.01, for trace ['review request', 'calculate terms', 'credit requested', 'calculate terms', 'assess risks', 'prepare contract', 'review request', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.01, for trace ['credit requested', 'review request', 'review request', 'prepare contract', 'assess risks', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.02, for trace ['credit requested', 'calculate terms', 'review request', 'review request', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.01, for trace ['credit requested', 'review request', 'review request', 'assess risks', 'assess risks', 'calculate terms', 'prepare contract', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.006666666666666666, for trace ['send quote', 'credit requested', 'review request', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'review request', 'quote sent']\n", + "Contribution is: 0.01, for trace ['credit requested', 'review request', 'review request', 'calculate terms', 'calculate terms', 'assess risks', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Total distribution to the fitness is 0.8933333333333341\n", + "Our total fitness rate is 0.8933333333333333\n" + ] + } + ], + "source": [ + "total_distribution = 0\n", + "i = 0\n", + "for trace in exp.event_log.get_traces():\n", + " i +=1\n", + " ctrb = exp.variant_ctrb_to_fitness(\n", + " event_log=exp.event_log,\n", + " trace=trace,\n", + " )\n", + " total_distribution += ctrb\n", + " # Let's just show some traces\n", + " if i % 10 == 0:\n", + " print(f\"Contribution is: {ctrb}, for trace {trace.nodes}\")\n", + "print(f\"Total distribution to the fitness is {total_distribution}\")\n", + "print(f\"Our total fitness rate is {fit_rate}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`constraint_ctrb_to_fitness` determines how much a specific constraint contributes to the overall fitness rate" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution is: 0.26666666666666666, for constrainst (^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\n", + "Contribution is: 0.31666666666666665, for constrainst (^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\n", + "Contribution is: 0.31, for constrainst (^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\n", + "Our total contribution to the fitness rate is: 0.8933333333333333\n", + "Compared to our fitness rate : 0.8933333333333333\n" + ] + } + ], + "source": [ + "ctrb_0 = exp.constraint_ctrb_to_fitness(exp.event_log, exp.constraints, 0)\n", + "ctrb_1 = exp.constraint_ctrb_to_fitness(exp.event_log, exp.constraints, 1)\n", + "ctrb_2 = exp.constraint_ctrb_to_fitness(exp.event_log, exp.constraints, 2)\n", + "\n", + "print(f\"Contribution is: {ctrb_0}, for constrainst {exp.constraints[0]}\")\n", + "print(f\"Contribution is: {ctrb_1}, for constrainst {exp.constraints[1]}\")\n", + "print(f\"Contribution is: {ctrb_2}, for constrainst {exp.constraints[2]}\")\n", + "\n", + "print(f\"Our total contribution to the fitness rate is: {ctrb_0 + ctrb_1 + ctrb_2}\")\n", + "print(f\"Compared to our fitness rate : {fit_rate}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 7: Shapely values\n", + "\n", + "`constraint_ctrb_to_conformance` determines how much a specific constraint contributes to the overall conformance loss. \n", + "\n", + "Because the constraints overlap in this case, Shapley values have been used to determine the contribution. This makes the method more complicated and more computationally heavy than the other contribution functions \n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution is: 0.19, for constrainst (^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\n", + "Contribution is: 0.040000000000000036, for constrainst (^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\n", + "Contribution is: 0.06, for constrainst (^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\n", + "Our total contribution to the conformance loss is: 0.29000000000000004\n", + "Compared to our conformance loss : 0.29000000000000004\n" + ] + } + ], + "source": [ + "ctrb_0 = exp.constraint_ctrb_to_conformance(exp.event_log, exp.constraints, 0)\n", + "ctrb_1 = exp.constraint_ctrb_to_conformance(exp.event_log, exp.constraints, 1)\n", + "ctrb_2 = exp.constraint_ctrb_to_conformance(exp.event_log, exp.constraints, 2)\n", + "\n", + "print(f\"Contribution is: {ctrb_0}, for constrainst {exp.constraints[0]}\")\n", + "print(f\"Contribution is: {ctrb_1}, for constrainst {exp.constraints[1]}\")\n", + "print(f\"Contribution is: {ctrb_2}, for constrainst {exp.constraints[2]}\")\n", + "\n", + "print(f\"Our total contribution to the conformance loss is: {ctrb_0 + ctrb_1 + ctrb_2}\")\n", + "print(f\"Compared to our conformance loss : {1 - conf_rate}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 8: Combining BPMN2CONSTRAINTS and the Explainer" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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94tyeLxlebiW5dl8v0CvlRBCCCFE8UIxKoQQQgghhBBCCJE0SKgQQgghhBBCCCFE0qCpH0IIkViIYzEvzn0JLPm7kkwIIYQQQhQnJFQIIURieSMH+45QcgkhhBBCiOKGpn4IIYQQQgghhBAiaZBQIYQQQgghhBBCiKRBQoUQIreUUxIoDYQQKhuEUB4VQuQ3EiqEEDnm2GOPbVu5cuXVrVu37qQ0KL5pIIRQ2SCE8qgQoiCQUCGEyHEF/8svvwy//fbbS3/yyScvnnLKKe2VBsUvDYQQKhuEUB4VQhQUEiqEEDmu4EeNGlWqe/fuZuTIkSW//fbbYQ0aNDhTaVB80kAIobJBCOVRIURBIqFCpAR///23+f3335UQCargTzrpJG8bf6nof/rpp9cPO+ywVkqD1E8DkTn2HTAXXXSRmT9/foEcf9WqVd7xv/zySyW2ygYhhPJosea6664zr732mhIihSmlJBD5yT333GNeeOGFjM/lypUz++yzjzn55JPN5ZdfbnbccccCOe8VV1xhxowZY/766y89hARV8AF8ZvuZZ545qk6dOm1mz549UWmQkDS439of1h6J8d3/rB1prZu1dFfWX2KtjbWdrM2z1s/atNBv0qx1sXaWtV2sbbD2rTVaASnbM165cqXp06ePGTt2rPn555+9bVWqVDH16tUzjLideuqpcR9r6dKlXvl39dVXm7333jvfr3XdunXe8Zs3b877qIJJZUOxo3///uaxxx7L+FypUiUvr51++umma9eupnTp0kokoTxaAEyaNMncf//9nlD+77//mp122skceeSRXtv+3HPPLZRrev31103JkiUL7fxCQkWB8PLLL5/xxRdfXDl9+vR6f/75ZwXbUC29ePHiEjVq1NhctWrVDbYzvbJatWpj58yZ89i8efN+02sSP3/88YdZvny5uemmmzK2ff/9997noUOHms8++8yUKVMm389LI6VixYp6AAmu4KMVfbt27cbsueeerX7//fe3lAYFngZbrN1n7Tlrq0PbS1rrZe1tJ1IgQLxuram1p6z95gSLT6y1tDY1JHxca+1Za+Os7cltWZuTqkLF119/bVq0aGHWr1/veSrcdtttXkfn119/9YSL9PT0lLlXGpZHHXWUmTFjhgo1lY9FFttmM7/99pu56667MrbZ9py58sorzfPPP28++OADU7ZsWSWUUB7NR8aPH2/atGljmjRpYp5++mmvvb1o0SIzZcoUT6AXhU5Na3gEtbBWw9pu1vawttDaEms8pImubbdIQkUSYhucOzzyyCN9RowYccW1115b+owzzjDdunXzlPgaNWqYPfbYwyxcuLCUzXAV58+fX9FmyuumTZt27X777fe7/e2Vc+fOnaB8EB94TYQbEfDGG2+YDh06mFdffdVceOGF+X7Ojh07eiYSX8GHK/rRo0fv0LZt2zerVavWctWqVVOVBgWaBggKt/H6W3sytP1kV0k97z53sNbW2gm06d22QdamWBto7cAgG7nj9CgO7zbeV61bt/bKq08//dTYsn6b72+//faUut8PP/zQm5oiVD4WddLS0rZrYzAQcsEFF5gXX3zRXHbZZUokoTyajzz++OOmTp06njBRosR/UQOYeiEKlSbW7rHWKJPv93BmnJDBYNVH1ihA35NQkSS89NJLFzZt2vSJhQsXlrvvvvvMWWed5bkKbfc099jDs2OOOcacffbZZsuWLWkjR46sdeutt47dZ599fpw3b147u9uvyhc558wzz/QKt5kzZ26znTnXjz76qNdRgOOOO8706NHD2ErCGwFklOSUU07xRI4oTDNh9NM+H7xkzHfffWf69u27zT54cDzxxBOe8ss0lPbt23uNGRo6zCPnGLhpH3300Rm/4VoGDx5sOnfu7E1ZCeDaH3nkEa8DU7t2bVXwmVT0Y8aMKd2mTZvxFSpUaLFu3bp3lQYFlgYLrDHq0zUiVHQ2/tSOD9xnll/7PCRSAK4CI6w9bW0ftz+eFi3dtpQv52h4LV682LzzzjvbiRSxYFoI5cyPP/7oeY7tvPPOnqs50zCyY8WKFWbAgAFe2bJ161az6667mvPPP98gmCMePPzww+bOO+80++677za/e+qppzwPtWinLMrkyZPNuHHjzNy5c83GjRvNwQcfbG644YaM+xoxYoS5++67vXPjOQKci3MG2Aa618lDwOHe6OxR9gqVj0WBTp06mauuuspMmzYtQ6i4/vrrvQGMmjVrmnvvvdfMnj3by3M33nij9z2eVORLygDb3jOHHHKI1/6oVatWxnEnTJhgpk+f7g1s0b5A8IP69et7U8Noq4Qhjw8fPtzYd8ObqsVg2DXXXOO5yAe89dZbnvs8+W/IkCHeHPvNmzebiRMnZkyPfffdd80zzzzj5X9G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GqQaeX2vhyXqQidPnuzZY489pgIx7+VZUtWLyVo22MZrUq36URzbHKeccop54YUX8hxQN7ewtOMxxxyz3RLqRTmPplIbJgd5NNftl0TXdckAq+6wAtWFF15YVOu6vJabrPpxDBu//vrrAg2oyfFD/J4saZxSQoXNxCMnTpxYN5HLkz777LO8hG8X0v16BZt74dPiWZZRTejcM2vWLPPJJ59ss450YZNdxR7ruf/+++9vVatWrWWbNm3Gjx07tkxhVPTRDnqeWj1xiBP5kQY9evTIWKIOgojsqNDr1q3ztm3cuNHccMMNZsiQIQlNg1QjN+91okCoDJ63SMp2QJ7qRZUNanNEmT9/vhe0OOotk0iGDRtmDjroIJX1KdaGyU37pTjx2muvma5duxbnOon+pbc86ZgxYwp0eVKOHzmvhIr8Zvr06YMWLFhwu30RSqJcFTT2PH///PPPh9l/zy+M+02WAo6GFxnx/PPPN3369DFvv/22t9woBUy4sHnllVe8RhvfMfp04oknRtPTPPfcc2bUqFFeo46lZm+55RYzZ84c8/3335ubb77Z22/GjBnm0Ucf9SIxR5cKe+KJJwiq6s0J3ibH2WsaPHiwWbFiBXO/zEUXXWTatm27zT4fffSRtw/rNMP+++9vbJp6+02ZMiXDjfC6667zlvHD7euRRx4plHc9r8991apVUytUqNCidevWE958882yiazo86uCT2QafP755947vHZtRgBmU65cOa/xeuSRR2Z0RjZs2OC5eF9yySWmfv36EikS/EwLEp4x5RvvAUGnKEPg3HPPJVZRxj4DBgww7777rlee4TJOJza8pNr48ePNzJkzvbKNVSKGDx/uNdYpY+w76W3v16+ftx9lJmVh7dq1ze233+51fpliwLzYv//+24vSTeeXkdYAOlMcd9KkSd5IftmyZb1RmE6dOqVkR6cg6kWVDcnb5pg9e7YZOHCg1w6AffbZx/NWqVevXsY18jywNWvWeEvD4oVwwQUXeK7W4XxIIDzq8/79+3vPi3xIXiPP1qnje2yTl4O6n+32vfDs8ccf9zxp+D2jvohSDzzwgNfGIE8S9BHxasSIEea9994zv/76q3eM4447znTv3n27ZRuXLl3qlR28T9xDMIq87777ml69epnffvvNa59wPXD//fcX6dgZxb0NU1TECYT5559/3ivfeN+u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F8yVV14poUIIIYQQSYuECiGEEIXKTTfddFjjxo33bNGiRR37sUt6evov9u8nphiJFjmlffv2pnr16koIIYQQQqQkEiqEEEIUKn369DnYmqlYseLm5s2bL+7SpUuVFi1a1DYSLTLl3nvvVSIIIYQQImWRUCGEECIpWLt2bak33nijFpYsosXHH39sBg8ebBYsWOB9rl27trHXY9q1a+d9njhxovn222/Ntddea/r27Ws++OADb/txxx1nevTosd3SpP/++695+umnzYQJE8ymTZu86Rfsd9BBB22z39atW82wYcPM8OHDzZo1a0y5cuXM0Ucfbe666y5TsmRJT6jYeeedvSkcAbNnzzYvv/yy+eGHH8wff/zhnbtr167e9QohhBBCSKgQQgghEidaFMg1TJkyJUOUaNOmjScYTJs2zYwYMSJDqJg5c6bp37+/GTt2rBczolmzZmblypVm0KBBZuTIkebLL780VapUyRAfWrVqZb766itz6aWXmt12283bp169euadd97xxA2w92Y6duxoRo0aZc466yxz1FFHmYULF5qffvrJEylg/PjxnsgRFipuvPFG89dff5kmTZqYGjVqeNdEwE725a8QQgghhIQKIYQoRtjO5TilQu7ITmgIixaVK1dOb9++/cbOnTuf0KhRo47GD8JZILz22mvmiCOO8LwasmLFihXmoosu8lbTCDj//PM9AYJtvXv39rY9++yz5t133zVff/11RhDNq666ytSvX99cd9115rPPPvO2cT57r975O3ToEPf1IqCUKVMm4/Nll11mDjjgADN06NCEChX/b+9OwGs+0/+P30cSEUVsY40wSkaNMh2qhFTQRku1ttqi9upM+0c7xNpSE1Nqi6SWoqYyat9rKzPGhSmpdiwNqkZRKrFTIY1EnN/3firnnxBbYvme5P26ruc62zffnDwni/NxP/ejIcvcuXP5eQAAAAQVAPAoWG8ui1lvNgOYiYfjwoULjunTp/voyJcvn7NKlSoFHtTn8vf3N6HBxo0bTZXC7fTs2TPD7Ro1akhISIipakgLKubMmSPPPfdchp0+tEKiTZs2MnjwYLNco2jRoiagCAgIuKeQQqUPKVSePHmkevXqZivTh0W/hn379klu/pnQ3wn8pAIAkD0EFQBw71KsN4XX9MrOnTuL6WBKHr6kpCTHrl27HntQ5+/fv7+p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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import sys\n", + "sys.path.append(\"../../\")\n", + "\n", + "from bpmnconstraints.script import compile_bpmn_diagram\n", + "import SignavioAuthenticator\n", + "import requests\n", + "from IPython.display import Image, display\n", + "import json\n", + "import logging\n", + "from conf import *\n", + "logging.getLogger(\"urllib3\").setLevel(logging.WARNING)\n", + "\n", + "system_instance = 'https://editor.signavio.com'\n", + "workspace_id = workspace_id# workspace id\n", + "user_name = user_name# username\n", + "pw = pw # pw\n", + "revision_id = '1fe7397c17304d3ba4ea41f1eefc97fe'\n", + "authenticator = SignavioAuthenticator.SignavioAuthenticator(system_instance, workspace_id, user_name, pw)\n", + "auth_data = authenticator.authenticate()\n", + "cookies = {'JSESSIONID': auth_data['jsesssion_ID'], 'LBROUTEID': auth_data['lb_route_ID']}\n", + "headers = {'Accept': 'application/json', 'x-signavio-id': auth_data['auth_token']}\n", + "diagram_url = system_instance + '/p/revision'\n", + "\n", + "json_request = requests.get(\n", + " f'{diagram_url}/{revision_id}/json',\n", + " cookies=cookies,\n", + " headers=headers)\n", + "json_diagram = json_request.content\n", + "path = './diagram.json'\n", + "with open(path, 'w') as f:\n", + " json.dump(json.loads(json_diagram), f)\n", + "png_request = requests.get(\n", + " f'{diagram_url}/{revision_id}/png',\n", + " cookies=cookies,\n", + " headers=headers)\n", + "display(Image(png_request.content))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO:root:Generating SIGNAL constraints...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 119/119 [00:00<00:00, 2365507.94it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\"(^'review request')\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('standard terms applicable'|'assess risks')*(('standard terms applicable'ANY*'assess risks'ANY*)|('assess risks'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'assess risks')*$)\", \"(^NOT('standard terms applicable'|'prepare special terms')*(('standard terms applicable'ANY*'prepare special terms'ANY*)|('prepare special terms'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'prepare special terms')*$)\", \"(^NOT('standard terms applicable'|'calculate terms')*(('standard terms applicable'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'calculate terms')*$)\", \"(^NOT('assess risks'|'prepare special terms')*(('assess risks'ANY*'prepare special terms'ANY*)|('prepare special terms'ANY* 'assess risks' ANY*))* NOT('assess risks'|'prepare special terms')*$)\", \"(^NOT('assess risks'|'calculate terms')*(('assess risks'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'assess risks' ANY*))* NOT('assess risks'|'calculate terms')*$)\", \"(^NOT('prepare special terms'|'calculate terms')*(('prepare special terms'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'calculate terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'assess risks'))*$)\", \"(^NOT('assess risks')*('review request' NOT('assess risks')*'assess risks'NOT('assess risks')*)*NOT('assess risks')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'prepare special terms'))*$)\", \"(^NOT('prepare special terms')*('review request' NOT('prepare special terms')*'prepare special terms'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'calculate terms'))*$)\", \"(^NOT('calculate terms')*('review request' NOT('calculate terms')*'calculate terms'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'prepare special terms'))*$)\", \"(^NOT('prepare special terms')*('review request' NOT('prepare special terms')*'prepare special terms'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'calculate terms'))*$)\", \"(^NOT('calculate terms')*('review request' NOT('calculate terms')*'calculate terms'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^(((NOT('prepare special terms')*) ('calculate terms' NOT('prepare special terms')*)*)|((NOT('calculate terms')*)('prepare special terms' NOT('calculate terms')*)*))$)\", \"(('prepare special terms'|'calculate terms'))\", \"(^NOT('calculate terms'|'prepare contract')*('calculate terms'~>'prepare contract')*NOT('calculate terms'|'prepare contract')*$)\", \"(^NOT('calculate terms'|'prepare contract')*(('calculate terms'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'calculate terms' ANY*))* NOT('calculate terms'|'prepare contract')*$)\", \"( ^ NOT('calculate terms'|'prepare contract')* ('calculate terms'NOT('calculate terms'|'prepare contract')*'prepare contract'NOT('calculate terms'|'prepare contract')*)*NOT('calculate terms'|'prepare contract')* $)\", \"(^NOT('calculate terms'|'send quote')*('calculate terms'~>'send quote')*NOT('calculate terms'|'send quote')*$)\", \"(^NOT('calculate terms'|'send quote')*(('calculate terms'ANY*'send quote'ANY*)|('send quote'ANY* 'calculate terms' ANY*))* NOT('calculate terms'|'send quote')*$)\", \"( ^ NOT('calculate terms'|'send quote')* ('calculate terms'NOT('calculate terms'|'send quote')*'send quote'NOT('calculate terms'|'send quote')*)*NOT('calculate terms'|'send quote')* $)\", \"(^NOT('prepare contract'|'send quote')*('prepare contract'~>'send quote')*NOT('prepare contract'|'send quote')*$)\", \"(^NOT('prepare contract'|'send quote')*(('prepare contract'ANY*'send quote'ANY*)|('send quote'ANY* 'prepare contract' ANY*))* NOT('prepare contract'|'send quote')*$)\", \"(('prepare contract'|'send quote'))\", \"( ^ NOT('prepare contract'|'send quote')* ('prepare contract'NOT('prepare contract'|'send quote')*'send quote'NOT('prepare contract'|'send quote')*)*NOT('prepare contract'|'send quote')* $)\", \"(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\", \"(^NOT('prepare contract')*('prepare contract'NOT('prepare contract')*'send quote'NOT('prepare contract')*)*NOT('prepare contract')*$)\", \"(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\", \"(^NOT('assess risks')*('assess risks'NOT('assess risks')*'send quote'NOT('assess risks')*)*NOT('assess risks')*$)\", \"('send quote'$)\", \"(^NOT('prepare special terms'|'prepare contract')*('prepare special terms'~>'prepare contract')*NOT('prepare special terms'|'prepare contract')*$)\", \"(^NOT('prepare special terms'|'prepare contract')*(('prepare special terms'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'prepare contract')*$)\", \"( ^ NOT('prepare special terms'|'prepare contract')* ('prepare special terms'NOT('prepare special terms'|'prepare contract')*'prepare contract'NOT('prepare special terms'|'prepare contract')*)*NOT('prepare special terms'|'prepare contract')* $)\", \"(^NOT('prepare special terms'|'send quote')*('prepare special terms'~>'send quote')*NOT('prepare special terms'|'send quote')*$)\", \"(^NOT('prepare special terms'|'send quote')*(('prepare special terms'ANY*'send quote'ANY*)|('send quote'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'send quote')*$)\", \"( ^ NOT('prepare special terms'|'send quote')* ('prepare special terms'NOT('prepare special terms'|'send quote')*'send quote'NOT('prepare special terms'|'send quote')*)*NOT('prepare special terms'|'send quote')* $)\", \"(^NOT('assess risks'|'send quote')*('assess risks'~>'send quote')*NOT('assess risks'|'send quote')*$)\", \"(^NOT('assess risks'|'send quote')*(('assess risks'ANY*'send quote'ANY*)|('send quote'ANY* 'assess risks' ANY*))* NOT('assess risks'|'send quote')*$)\", \"( ^ NOT('assess risks'|'send quote')* ('assess risks'NOT('assess risks'|'send quote')*'send quote'NOT('assess risks'|'send quote')*)*NOT('assess risks'|'send quote')* $)\", \"(^NOT('calculate terms')* ('calculate terms' ANY*'prepare contract')* NOT('calculate terms')*$)\", \"(^NOT('calculate terms')*('calculate terms'NOT('calculate terms')*'prepare contract'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^NOT('prepare special terms')* ('prepare special terms' ANY*'prepare contract')* NOT('prepare special terms')*$)\", \"(^NOT('prepare special terms')*('prepare special terms'NOT('prepare special terms')*'prepare contract'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\"]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "signal_constraints = compile_bpmn_diagram(path, \"SIGNAL\", False)\n", + "print(signal_constraints)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conformance rate: 0.0\n", + "Fitness rate : 0.6016806722689075\n" + ] + } + ], + "source": [ + "exp = ExplainerSignal() #Reset the explainer\n", + "exp.set_endpoint('/g/api/pi-graphql/signal')\n", + "for con in signal_constraints:\n", + " exp.add_constraint(con)\n", + " \n", + "conf_rate = exp.determine_conformance_rate()\n", + "fit_rate = exp.determine_fitness_rate()\n", + "print(\"Conformance rate: \" + str(conf_rate))\n", + "print(\"Fitness rate : \" + str(fit_rate))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution is: 0.01, for trace ['review request', 'calculate terms', 'credit requested', 'calculate terms', 'assess risks', 'prepare contract', 'review request', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.01, for trace ['credit requested', 'review request', 'review request', 'prepare contract', 'assess risks', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.02, for trace ['credit requested', 'calculate terms', 'review request', 'review request', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.01, for trace ['credit requested', 'review request', 'review request', 'assess risks', 'assess risks', 'calculate terms', 'prepare contract', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.01, for trace ['send quote', 'credit requested', 'review request', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'review request', 'quote sent']\n", + "Contribution is: 0.01, for trace ['credit requested', 'review request', 'review request', 'calculate terms', 'calculate terms', 'assess risks', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Total distribution to the conformance loss is 1.0000000000000004\n", + "Our total conformance loss is 1.0\n" + ] + } + ], + "source": [ + "total_distribution = 0\n", + "i = 0\n", + "for trace in exp.event_log.get_traces():\n", + " i +=1\n", + " ctrb = exp.variant_ctrb_to_conformance_loss(\n", + " event_log=exp.event_log,\n", + " trace=trace,\n", + " )\n", + " total_distribution += ctrb\n", + " # Let's just show some traces\n", + " if i % 10 == 0:\n", + " print(f\"Contribution is: {ctrb}, for trace {trace.nodes}\")\n", + "print(f\"Total distribution to the conformance loss is {total_distribution}\")\n", + "print(f\"Our total conformance loss is {1 - conf_rate}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution is: 0.007142857142857143, for trace ['review request', 'calculate terms', 'credit requested', 'calculate terms', 'assess risks', 'prepare contract', 'review request', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.006134453781512605, for trace ['credit requested', 'review request', 'review request', 'prepare contract', 'assess risks', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.012436974789915966, for trace ['credit requested', 'calculate terms', 'review request', 'review request', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.006386554621848739, for trace ['credit requested', 'review request', 'review request', 'assess risks', 'assess risks', 'calculate terms', 'prepare contract', 'calculate terms', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Contribution is: 0.004453781512605042, for trace ['send quote', 'credit requested', 'review request', 'calculate terms', 'assess risks', 'calculate terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'review request', 'quote sent']\n", + "Contribution is: 0.006218487394957983, for trace ['credit requested', 'review request', 'review request', 'calculate terms', 'calculate terms', 'assess risks', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "Total distribution to the fitness is 0.6016806722689076\n", + "Our total fitness rate is 0.6016806722689075\n" + ] + } + ], + "source": [ + "total_distribution = 0\n", + "i = 0\n", + "for trace in exp.event_log.get_traces():\n", + " i +=1\n", + " ctrb = exp.variant_ctrb_to_fitness(\n", + " event_log=exp.event_log,\n", + " trace=trace,\n", + " )\n", + " total_distribution += ctrb\n", + " # Let's just show some traces\n", + " if i % 10 == 0:\n", + " print(f\"Contribution is: {ctrb}, for trace {trace.nodes}\")\n", + "print(f\"Total distribution to the fitness is {total_distribution}\")\n", + "print(f\"Our total fitness rate is {fit_rate}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contribution is: 0.0067226890756302525, for constraint (^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\n", + "Contribution is: 0.008403361344537815, for constraint (('review request'|'prepare contract'))\n", + "Contribution is: 0.007563025210084034, for constraint (^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\n", + "Contribution is: 0.008403361344537815, for constraint (('review request'|'prepare contract'))\n", + "Contribution is: 0.007563025210084034, for constraint (^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\n", + "Contribution is: 0.008403361344537815, for constraint (('review request'|'send quote'))\n", + "Contribution is: 0.007563025210084034, for constraint (^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\n", + "Contribution is: 0.000588235294117647, for constraint (^ (NOT 'review request' | ('review request' (NOT 'review request')* 'assess risks'))*$)\n", + "Contribution is: 0.008403361344537815, for constraint (^(((NOT('prepare special terms')*) ('calculate terms' NOT('prepare special terms')*)*)|((NOT('calculate terms')*)('prepare special terms' NOT('calculate terms')*)*))$)\n", + "Contribution is: 0.008403361344537815, for constraint (('prepare contract'|'send quote'))\n", + "Contribution is: 0.0015966386554621848, for constraint (^NOT('prepare special terms'|'send quote')*('prepare special terms'~>'send quote')*NOT('prepare special terms'|'send quote')*$)\n", + "Total distribution to the fitness is 0.6016806722689074\n", + "Our total fitness rate is 0.6016806722689075\n" + ] + } + ], + "source": [ + "total_distribution = 0\n", + "i = 0\n", + "for con in signal_constraints:\n", + " ctrb = exp.constraint_ctrb_to_fitness(\n", + " log=exp.event_log,\n", + " constraints=exp.constraints,\n", + " index=i\n", + " )\n", + " i +=1\n", + " total_distribution += ctrb\n", + " # Let's just show some traces\n", + " if i % 10 == 0:\n", + " print(f\"Contribution is: {ctrb}, for constraint {con}\")\n", + " \n", + "print(f\"Total distribution to the fitness is {total_distribution}\")\n", + "print(f\"Our total fitness rate is {fit_rate}\")" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "\"\"\"\n", + "total_distribution = 0\n", + "i = 0\n", + "for con in signal_constraints:\n", + " ctrb = exp.constraint_ctrb_to_conformance(\n", + " log=exp.event_log,\n", + " constraints=exp.constraints,\n", + " index=i\n", + " )\n", + " i +=1\n", + " total_distribution += ctrb\n", + " # Let's just show some traces\n", + " if i % 10 == 0:\n", + " print(f\"Contribution is: {ctrb}, for constraint {con}\")\n", + " \n", + "print(f\"Total distribution to the conformance loss is {total_distribution}\")\n", + "print(f\"Our total conformance loss is { 1 - conf_rate}\")\n", + "\"\"\"\n" + ] } ], "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" } }, "nbformat": 4, diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 1746327..a2618fb 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -1,5 +1,7 @@ +import sys +sys.path.append('../../') from explainer.explainer import * -from explainer.explainer_regex import ExplainerRegex +from explainer import ExplainerRegex # Test 1: Adding and checking constraints def test_add_constraint(): From f67caf376a269ada25ef616b58dd0e8728d80de8 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 10:53:13 +0200 Subject: [PATCH 36/54] Changed location of tests --- {tests/explainer => explainer/tests}/explainer_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename {tests/explainer => explainer/tests}/explainer_test.py (99%) diff --git a/tests/explainer/explainer_test.py b/explainer/tests/explainer_test.py similarity index 99% rename from tests/explainer/explainer_test.py rename to explainer/tests/explainer_test.py index a2618fb..1746327 100644 --- a/tests/explainer/explainer_test.py +++ b/explainer/tests/explainer_test.py @@ -1,7 +1,5 @@ -import sys -sys.path.append('../../') from explainer.explainer import * -from explainer import ExplainerRegex +from explainer.explainer_regex import ExplainerRegex # Test 1: Adding and checking constraints def test_add_constraint(): From edfc5d79857e78531a1f317445b00346eedda180 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:05:33 +0200 Subject: [PATCH 37/54] Linting --- explainer/explainer.py | 5 - explainer/explainer_signal.py | 322 ++++++++++++++++++++++------------ 2 files changed, 208 insertions(+), 119 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 10904fa..8b62a3b 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -57,11 +57,6 @@ def conformant(self, trace, constraints=None): # Implementation remains the same pass - @abstractmethod - def contradiction(self, check_multiple=False, max_length=10): - # Implementation remains the same - pass - @abstractmethod def minimal_expl(self, trace): # Implementation remains the same diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 0306bf0..9754ba2 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -7,42 +7,180 @@ from itertools import combinations, chain import requests from conf import system_instance, workspace_id, user_name, pw - KEYWORDS = [ - "ABS", "ALL", "ANALYZE", "AND", "ANY", "AS", "ASC", "AVG", "BARRIER", "BEHAVIOUR", "BETWEEN", "BOOL_AND", - "BOOL_OR", "BUCKET", "BY", "CASE", "CASE_ID", "CATEGORY", "CEIL", "CHAR_INDEX", "CHAR_LENGTH", "COALESCE", - "CONCAT", "COUNT", "CREATE", "CURRENT", "DATE_ADD", "DATE_DIFF", "DATE_PART", "DATE_TRUNC", "DEFAULT", - "DENSE_RANK", "DESC", "DESCRIBE", "DISTINCT", "DROP", "DURATION", "DURATION_BETWEEN", "DURATION_FROM_DAYS", - "DURATION_FROM_MILLISECONDS", "DURATION_TO_DAYS", "DURATION_TO_MILLISECONDS", "ELSE", "END", "END_TIME", - "EVENT_ID", "EVENT_NAME", "EVENTS", "EXACT", "EXPLAIN", "EXTERNAL", "FALSE", "FILL", "FILTER", "FIRST", - "FLATTEN", "FLOOR", "FOLLOWING", "FORMAT", "FROM", "GRANT", "GROUP", "HAVING", "IF", "ILIKE", "IN", "INVOKER", - "IS", "JOIN", "JSON", "LAG", "LAST", "LEAD", "LEFT", "LIKE", "LIMIT", "LOCATION", "LOG", "MATCHES", "MAX", - "MEDIAN", "MIN", "NOT", "NOW", "NULL", "NULLS", "OCCURRENCE", "ODATA", "OFFSET", "ON", "ONLY", "OR", "ORDER", - "OUTER", "OVER", "PARQUET", "PARTITION", "PERCENT", "PERCENTILE_CONT", "PERCENTILE_DESC", "PERMISSIONS", "POW", - "PRECEDING", "PRIVATE", "PUBLIC", "RANGE", "RANK", "REGR_INTERCEPT", "REGR_SLOPE", "REPEATABLE", "REPLACE", - "RIGHT", "ROUND", "ROW", "ROW_NUMBER", "ROWS", "SECURITY", "SELECT", "SIGN", "SQRT", "START_TIME", "STDDEV", - "SUBSTRING", "SUBSTRING_AFTER", "SUBSTRING_BEFORE", "SUM", "TABLE", "TABULAR", "TEXT", "THEN", "TIMESERIES", - "TIMESTAMP", "TO", "TO_NUMBER", "TO_STRING", "TO_TIMESTAMP", "TRUE", "TRUNC", "UNBOUNDED", "UNION", "USING", - "VIEW", "WHEN", "WHERE", "WITH", "WITHIN", "" + "ABS", + "ALL", + "ANALYZE", + "AND", + "ANY", + "AS", + "ASC", + "AVG", + "BARRIER", + "BEHAVIOUR", + "BETWEEN", + "BOOL_AND", + "BOOL_OR", + "BUCKET", + "BY", + "CASE", + "CASE_ID", + "CATEGORY", + "CEIL", + "CHAR_INDEX", + "CHAR_LENGTH", + "COALESCE", + "CONCAT", + "COUNT", + "CREATE", + "CURRENT", + "DATE_ADD", + "DATE_DIFF", + "DATE_PART", + "DATE_TRUNC", + "DEFAULT", + "DENSE_RANK", + "DESC", + "DESCRIBE", + "DISTINCT", + "DROP", + "DURATION", + "DURATION_BETWEEN", + "DURATION_FROM_DAYS", + "DURATION_FROM_MILLISECONDS", + "DURATION_TO_DAYS", + "DURATION_TO_MILLISECONDS", + "ELSE", + "END", + "END_TIME", + "EVENT_ID", + "EVENT_NAME", + "EVENTS", + "EXACT", + "EXPLAIN", + "EXTERNAL", + "FALSE", + "FILL", + "FILTER", + "FIRST", + "FLATTEN", + "FLOOR", + "FOLLOWING", + "FORMAT", + "FROM", + "GRANT", + "GROUP", + "HAVING", + "IF", + "ILIKE", + "IN", + "INVOKER", + "IS", + "JOIN", + "JSON", + "LAG", + "LAST", + "LEAD", + "LEFT", + "LIKE", + "LIMIT", + "LOCATION", + "LOG", + "MATCHES", + "MAX", + "MEDIAN", + "MIN", + "NOT", + "NOW", + "NULL", + "NULLS", + "OCCURRENCE", + "ODATA", + "OFFSET", + "ON", + "ONLY", + "OR", + "ORDER", + "OUTER", + "OVER", + "PARQUET", + "PARTITION", + "PERCENT", + "PERCENTILE_CONT", + "PERCENTILE_DESC", + "PERMISSIONS", + "POW", + "PRECEDING", + "PRIVATE", + "PUBLIC", + "RANGE", + "RANK", + "REGR_INTERCEPT", + "REGR_SLOPE", + "REPEATABLE", + "REPLACE", + "RIGHT", + "ROUND", + "ROW", + "ROW_NUMBER", + "ROWS", + "SECURITY", + "SELECT", + "SIGN", + "SQRT", + "START_TIME", + "STDDEV", + "SUBSTRING", + "SUBSTRING_AFTER", + "SUBSTRING_BEFORE", + "SUM", + "TABLE", + "TABULAR", + "TEXT", + "THEN", + "TIMESERIES", + "TIMESTAMP", + "TO", + "TO_NUMBER", + "TO_STRING", + "TO_TIMESTAMP", + "TRUE", + "TRUNC", + "UNBOUNDED", + "UNION", + "USING", + "VIEW", + "WHEN", + "WHERE", + "WITH", + "WITHIN", + "", ] class ExplainerSignal(Explainer): def __init__(self): super().__init__() - self.authenticator = SignavioAuthenticator.SignavioAuthenticator(system_instance, workspace_id, user_name, pw) + self.authenticator = SignavioAuthenticator.SignavioAuthenticator( + system_instance, workspace_id, user_name, pw + ) self.auth_data = self.authenticator.authenticate() - self.cookies = {'JSESSIONID': self.auth_data['jsesssion_ID'], 'LBROUTEID': self.auth_data['lb_route_ID']} - self.headers = {'Accept': 'application/json', 'x-signavio-id': self.auth_data['auth_token']} + self.cookies = { + 'JSESSIONID': self.auth_data['jsesssion_ID'], + 'LBROUTEID': self.auth_data['lb_route_ID'] + } + self.headers = { + 'Accept': 'application/json', + 'x-signavio-id': self.auth_data['auth_token'] + } self.event_log = EventLog() self.signal_endpoint = None - self.cache = {} + self.cache = {} - - def set_endpoint(self, endpoint= '/g/api/pi-graphql/signal'): + def set_endpoint(self, endpoint= "/g/api/pi-graphql/signal"): self.signal_endpoint = system_instance + endpoint self.load_variants() - + def remove_constraint(self, idx): """ Removes a constraint by index and updates the nodes list if necessary. @@ -72,10 +210,14 @@ def conformant(self, trace, constraints=None): if not constraints: constraints = self.constraints if len(constraints) > 1: - constraints = " AND ".join([f"event_name MATCHES {constraint}" for constraint in constraints]) + constraints = " AND ".join( + [f"event_name MATCHES {constraint}" for constraint in constraints] + ) else: constraints = "".join(f"event_name MATCHES {constraints[0]}") - query = f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' + query = ( + f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' + ) conformant = False cache_key = hash(query) if cache_key in self.cache: @@ -84,12 +226,12 @@ def conformant(self, trace, constraints=None): conformant = True break return conformant - + query_request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query} + json={'query': query}, ) result = query_request.json()['data'] self.cache[cache_key] = result # Store the result in cache @@ -99,10 +241,7 @@ def conformant(self, trace, constraints=None): conformant = True break return conformant - - def contradiction(self, check_multiple=False, max_length=10): - pass - + def minimal_expl(self, trace): if self.conformant(trace): @@ -122,19 +261,23 @@ def minimal_expl(self, trace): return "Non-conformance due to: " + explanations else: return "Trace is non-conformant, but the specific constraint violation could not be determined." - + def counterfactual_expl(self, trace): constraints = self.constraints if len(constraints) > 1: - constraints = " AND ".join([f"event_name MATCHES {constraint}" for constraint in constraints]) + constraints = " AND ".join( + [f"event_name MATCHES {constraint}" for constraint in constraints] + ) else: constraints = "".join(f"event_name MATCHES {constraints[0]}") - query = f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' + query = ( + f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' + ) query_request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query} + json={'query': query}, ) result = query_request.json()['data'] @@ -171,7 +314,6 @@ def operate_on_trace(self, trace, score, explanation_path, depth=0): explanation_string = explanation_path + "\n" + str(explanation) return self.counter_factual_helper(best_subtrace, explanation_string, depth + 1) - def modify_subtrace(self, trace): """ Modifies the given trace to meet constraints by adding nodes where the pattern fails. @@ -213,6 +355,7 @@ def modify_subtrace(self, trace): ] ) return potential_subtraces + def get_nodes_from_constraint(self, constraint=None): """ Extracts unique nodes from a constraint pattern. @@ -227,15 +370,17 @@ def get_nodes_from_constraint(self, constraint=None): return list(set(all_nodes)) else: return list(set(self.filter_keywords(constraint))) - + def filter_keywords(self, text): text = re.sub(r'\s+', '_', text.strip()) words = re.findall(r"\b[A-Z_a-z]+\b", text) modified_words = [word.replace("_", " ") for word in words] - filtered_words = [word for word in modified_words if word.strip() not in KEYWORDS] - + filtered_words = [ + word for word in modified_words if word.strip() not in KEYWORDS + ] + return filtered_words - + def evaluate_similarity(self, trace, cmp_trace=None): """ Calculates the similarity between the adherent trace and the given trace using the Levenshtein distance. @@ -251,7 +396,7 @@ def evaluate_similarity(self, trace, cmp_trace=None): max_distance = max(length, trace_len) normalized_score = 1 - lev_distance / max_distance return normalized_score - + def determine_conformance_rate(self, event_log = None, constraints=None): if constraints == None: constraints = self.constraints @@ -261,21 +406,19 @@ def determine_conformance_rate(self, event_log = None, constraints=None): non_conformant = self.check_violations(constraints) len_log = self.get_total_cases() - + return (len_log - non_conformant) / len_log - def determine_fitness_rate(self, event_log = None, constraints = None): + def determine_fitness_rate(self, event_log=None, constraints=None): if not constraints: constraints = self.constraints len_log = self.get_total_cases() total_conformance = 0 for con in constraints: total_conformance += self.check_conformance(con) - return total_conformance / (len_log * len(constraints)) - def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): if not self.constraints and not constraints: return "The explainer have no constraints" @@ -287,7 +430,7 @@ def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): contribution_of_trace = event_log.get_variant_count(trace) return contribution_of_trace / total_traces - + def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): if not self.constraints and not constraints: return "The explainer have no constraints" @@ -302,8 +445,7 @@ def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): contribution_of_trace = contribution_of_trace / len(constraints) contribution_of_trace = nr * contribution_of_trace return contribution_of_trace / total_traces - - + def constraint_ctrb_to_conformance(self, log = None, constraints = None, index = -1): """Determines the Shapley value-based contribution of a constraint to a the overall conformance rate. @@ -340,7 +482,7 @@ def constraint_ctrb_to_conformance(self, log = None, constraints = None, index = ) sub_ctrbs.append(sub_ctrb) return sum(sub_ctrbs) - + def constraint_ctrb_to_fitness(self, log = None, constraints = None, index = -1): # Implementation remains the same if len(constraints) < index: @@ -355,7 +497,9 @@ def constraint_ctrb_to_fitness(self, log = None, constraints = None, index = -1) return ctrb_count / (len_log * len(constraints)) def check_conformance(self, constraint): - query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}' + query = ( + f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}' + ) cache_key = hash(query) if cache_key in self.cache: return self.cache[cache_key] @@ -363,14 +507,18 @@ def check_conformance(self, constraint): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}) + json={'query': query}), result = query_request.json()['data'][0][0] self.cache[cache_key] = result return result - + def check_violations(self, constraints): - combined_constraints = " OR ".join([f"NOT event_name MATCHES {constraint}" for constraint in constraints]) - query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' + combined_constraints = " OR ".join( + [f"NOT event_name MATCHES {constraint}" for constraint in constraints] + ) + query = ( + f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' + ) cache_key = hash(query) if cache_key in self.cache: return self.cache[cache_key] @@ -378,7 +526,7 @@ def check_violations(self, constraints): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query} + json={'query': query}, ) result = query_request.json()['data'][0][0] self.cache[cache_key] = result @@ -393,11 +541,12 @@ def get_total_cases(self): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}) + json={'query': query}, + ) case_count = count_request.json()['data'][0][0] self.cache[cache_key] = case_count return case_count - + def load_variants(self): query = 'SELECT Activity From "defaultview-4"' @@ -405,13 +554,12 @@ def load_variants(self): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query} + json={'query': query}, ) data = query_request.json()['data'] for activity in data: self.event_log.add_trace(Trace(activity[0])) - def get_event_log(): return f'Select * FROM "defaultview-4"' @@ -439,58 +587,4 @@ def get_iterative_subtrace(trace): sublists = [] for i in range(0, len(trace)): sublists.append(trace.nodes[0 : i + 1]) - - return sublists -""" -exp = ExplainerSignal() -exp.set_endpoint() -exp.add_constraint("(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)") -exp.add_constraint("(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)") -exp.add_constraint("(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)") -trace = Trace(['credit requested', 'review request', 'prepare special terms', 'assess risks', 'prepare special terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']) - -#print(exp.minimal_expl(trace)) -#print(exp.counterfactual_expl(trace)) - -print("Conf rate: " + str(exp.determine_conformance_rate())) -print("Fitness rate: " + str(exp.determine_fitness_rate())) - -total_distribution = 0 -for trace in exp.event_log.get_traces(): - ctrb = exp.variant_ctrb_to_fitness( - event_log=exp.event_log, - trace=trace, - ) - total_distribution += ctrb -print(f"Total distribution to the fitness rate is {total_distribution}") - - -total_distribution = 0 -for trace in exp.event_log.get_traces(): - ctrb = exp.variant_ctrb_to_conformance_loss( - event_log=exp.event_log, - trace=trace, - ) - total_distribution += ctrb - -print(f"Total distribution to the conformance loss is {total_distribution}") -event_log = EventLog() - -first_ctrb = exp.constraint_ctrb_to_fitness(constraints=exp.constraints, index = 0) -snd_ctrb = exp.constraint_ctrb_to_fitness(constraints=exp.constraints, index = 1) -thr_ctrb = exp.constraint_ctrb_to_fitness(constraints=exp.constraints, index = 2) -print(f"First constraint contribution to fitness: {first_ctrb}") -print(f"Second constraint contribution to fitness: {snd_ctrb}") -print(f"third constraint contribution to fitness: {thr_ctrb}") -print(f"total distributon to fitness: {first_ctrb + snd_ctrb + thr_ctrb}") -first_ctrb = exp.constraint_ctrb_to_conformance(index = 0) -snd_ctrb = exp.constraint_ctrb_to_conformance(index = 1) -thr_ctrb = exp.constraint_ctrb_to_conformance(index = 2) - - -print(f"First constraint contribution to conf: {first_ctrb}") -print(f"Second constraint contribution to conf: {snd_ctrb}") -print(f"Third constraint contribution to conf: {thr_ctrb}") - -print(f"total distributon to conf loss: {first_ctrb + snd_ctrb + thr_ctrb}") -""" \ No newline at end of file + return sublists \ No newline at end of file From 18c92943aff3eda06c74a94d7f3258216c530c38 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:09:12 +0200 Subject: [PATCH 38/54] Lint --- explainer/explainer_signal.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 9754ba2..4d889f7 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -7,6 +7,7 @@ from itertools import combinations, chain import requests from conf import system_instance, workspace_id, user_name, pw + KEYWORDS = [ "ABS", "ALL", @@ -166,18 +167,18 @@ def __init__(self): ) self.auth_data = self.authenticator.authenticate() self.cookies = { - 'JSESSIONID': self.auth_data['jsesssion_ID'], - 'LBROUTEID': self.auth_data['lb_route_ID'] + "JSESSIONID": self.auth_data["jsesssion_ID"], + "LBROUTEID": self.auth_data["lb_route_ID"] } self.headers = { - 'Accept': 'application/json', - 'x-signavio-id': self.auth_data['auth_token'] + "Accept": "application/json", + "x-signavio-id": self.auth_data["auth_token"] } self.event_log = EventLog() self.signal_endpoint = None self.cache = {} - def set_endpoint(self, endpoint= "/g/api/pi-graphql/signal"): + def set_endpoint(self, endpoint="/g/api/pi-graphql/signal"): self.signal_endpoint = system_instance + endpoint self.load_variants() @@ -231,9 +232,9 @@ def conformant(self, trace, constraints=None): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}, + json={"query": query}, ) - result = query_request.json()['data'] + result = query_request.json()["data"] self.cache[cache_key] = result # Store the result in cache for res in result: @@ -277,9 +278,9 @@ def counterfactual_expl(self, trace): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}, + json={"query": query}, ) - result = query_request.json()['data'] + result = query_request.json()["data"] best_score = -float("inf") for res in result: @@ -507,8 +508,8 @@ def check_conformance(self, constraint): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}), - result = query_request.json()['data'][0][0] + json={"query": query}), + result = query_request.json()["data"][0][0] self.cache[cache_key] = result return result @@ -526,9 +527,9 @@ def check_violations(self, constraints): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}, + json={"query": query}, ) - result = query_request.json()['data'][0][0] + result = query_request.json()["data"][0][0] self.cache[cache_key] = result return result @@ -541,9 +542,9 @@ def get_total_cases(self): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}, + json={"query": query}, ) - case_count = count_request.json()['data'][0][0] + case_count = count_request.json()["data"][0][0] self.cache[cache_key] = case_count return case_count @@ -554,9 +555,9 @@ def load_variants(self): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={'query': query}, + json={"query": query}, ) - data = query_request.json()['data'] + data = query_request.json()["data"] for activity in data: self.event_log.add_trace(Trace(activity[0])) From 13b1d1766a33b0a8ba036132d58258c893e12a24 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:13:24 +0200 Subject: [PATCH 39/54] Lint --- explainer/explainer_signal.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 4d889f7..6b033f4 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -168,11 +168,11 @@ def __init__(self): self.auth_data = self.authenticator.authenticate() self.cookies = { "JSESSIONID": self.auth_data["jsesssion_ID"], - "LBROUTEID": self.auth_data["lb_route_ID"] + "LBROUTEID": self.auth_data["lb_route_ID"], } self.headers = { "Accept": "application/json", - "x-signavio-id": self.auth_data["auth_token"] + "x-signavio-id": self.auth_data["auth_token"], } self.event_log = EventLog() self.signal_endpoint = None @@ -373,7 +373,7 @@ def get_nodes_from_constraint(self, constraint=None): return list(set(self.filter_keywords(constraint))) def filter_keywords(self, text): - text = re.sub(r'\s+', '_', text.strip()) + text = re.sub(r"\s+", "_", text.strip()) words = re.findall(r"\b[A-Z_a-z]+\b", text) modified_words = [word.replace("_", " ") for word in words] filtered_words = [ @@ -398,7 +398,7 @@ def evaluate_similarity(self, trace, cmp_trace=None): normalized_score = 1 - lev_distance / max_distance return normalized_score - def determine_conformance_rate(self, event_log = None, constraints=None): + def determine_conformance_rate(self, event_log=None, constraints=None): if constraints == None: constraints = self.constraints if constraints == []: @@ -409,8 +409,7 @@ def determine_conformance_rate(self, event_log = None, constraints=None): len_log = self.get_total_cases() return (len_log - non_conformant) / len_log - - + def determine_fitness_rate(self, event_log=None, constraints=None): if not constraints: constraints = self.constraints @@ -419,7 +418,7 @@ def determine_fitness_rate(self, event_log=None, constraints=None): for con in constraints: total_conformance += self.check_conformance(con) return total_conformance / (len_log * len(constraints)) - + def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): if not self.constraints and not constraints: return "The explainer have no constraints" @@ -427,7 +426,7 @@ def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): constraints = self.constraints total_traces = len(event_log) contribution_of_trace = 0 - if not self.conformant(trace, constraints= constraints): + if not self.conformant(trace, constraints=constraints): contribution_of_trace = event_log.get_variant_count(trace) return contribution_of_trace / total_traces @@ -447,7 +446,7 @@ def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): contribution_of_trace = nr * contribution_of_trace return contribution_of_trace / total_traces - def constraint_ctrb_to_conformance(self, log = None, constraints = None, index = -1): + def constraint_ctrb_to_conformance(self, log = None, constraints=None, index=-1): """Determines the Shapley value-based contribution of a constraint to a the overall conformance rate. Args: @@ -484,7 +483,7 @@ def constraint_ctrb_to_conformance(self, log = None, constraints = None, index = sub_ctrbs.append(sub_ctrb) return sum(sub_ctrbs) - def constraint_ctrb_to_fitness(self, log = None, constraints = None, index = -1): + def constraint_ctrb_to_fitness(self, log = None, constraints=None, index=-1): # Implementation remains the same if len(constraints) < index: raise Exception("Constraint not in constraint list.") @@ -498,9 +497,7 @@ def constraint_ctrb_to_fitness(self, log = None, constraints = None, index = -1) return ctrb_count / (len_log * len(constraints)) def check_conformance(self, constraint): - query = ( - f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}' - ) + query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}' cache_key = hash(query) if cache_key in self.cache: return self.cache[cache_key] @@ -508,7 +505,8 @@ def check_conformance(self, constraint): self.signal_endpoint, cookies=self.cookies, headers=self.headers, - json={"query": query}), + json={"query": query}, + ) result = query_request.json()["data"][0][0] self.cache[cache_key] = result return result @@ -561,9 +559,11 @@ def load_variants(self): for activity in data: self.event_log.add_trace(Trace(activity[0])) + def get_event_log(): return f'Select * FROM "defaultview-4"' + def determine_powerset(elements): """Determines the powerset of a list of elements Args: @@ -577,6 +577,7 @@ def determine_powerset(elements): ) return [set(ps_element) for ps_element in ps_elements] + def get_iterative_subtrace(trace): """ Generates all possible non-empty contiguous sublists of a list, maintaining order. @@ -588,4 +589,4 @@ def get_iterative_subtrace(trace): sublists = [] for i in range(0, len(trace)): sublists.append(trace.nodes[0 : i + 1]) - return sublists \ No newline at end of file + return sublists From a7831c910d33118f2a957d2ce40b2bf7e2cf371e Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:14:46 +0200 Subject: [PATCH 40/54] Lint --- explainer/explainer_signal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 6b033f4..0bafd87 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -172,7 +172,7 @@ def __init__(self): } self.headers = { "Accept": "application/json", - "x-signavio-id": self.auth_data["auth_token"], + "x-signavio-id": self.auth_data["auth_token"], } self.event_log = EventLog() self.signal_endpoint = None @@ -446,7 +446,7 @@ def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): contribution_of_trace = nr * contribution_of_trace return contribution_of_trace / total_traces - def constraint_ctrb_to_conformance(self, log = None, constraints=None, index=-1): + def constraint_ctrb_to_conformance(self, log=None, constraints=None, index=-1): """Determines the Shapley value-based contribution of a constraint to a the overall conformance rate. Args: @@ -483,7 +483,7 @@ def constraint_ctrb_to_conformance(self, log = None, constraints=None, index=-1) sub_ctrbs.append(sub_ctrb) return sum(sub_ctrbs) - def constraint_ctrb_to_fitness(self, log = None, constraints=None, index=-1): + def constraint_ctrb_to_fitness(self, log=None, constraints=None, index=-1): # Implementation remains the same if len(constraints) < index: raise Exception("Constraint not in constraint list.") From f804d5b0dbefa1fc781cd9f8acc4a975e4d0cd4e Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:17:01 +0200 Subject: [PATCH 41/54] Lint --- explainer/explainer.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 8b62a3b..aa01c45 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -1,5 +1,7 @@ from abc import ABC, abstractmethod from itertools import combinations + + class Explainer(ABC): def __init__(self): """ @@ -51,55 +53,56 @@ def activation(self, trace, constraints=None): :return: Boolean indicating if any constraint is activated. """ pass - + @abstractmethod def conformant(self, trace, constraints=None): # Implementation remains the same pass - + @abstractmethod def minimal_expl(self, trace): # Implementation remains the same pass - + @abstractmethod def counterfactual_expl(self, trace): # Implementation remains the same pass - + @abstractmethod def evaluate_similarity(self, trace, cmp_trace=None): # Implementation remains the same pass - + @abstractmethod def determine_conformance_rate(self, event_log, constraints=None): # Implementation remains the same pass - + @abstractmethod def determine_fitness_rate(self, event_log, constraints = None): pass - + @abstractmethod def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): # Implementation remains the same pass - + @abstractmethod def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): # Implementation remains the same pass - + @abstractmethod def constraint_ctrb_to_conformance(self, log, constraints, index): pass - + @abstractmethod def constraint_ctrb_to_fitness(self, log, constraints, index): # Implementation remains the same pass - + + class Trace: def __init__(self, nodes): """ @@ -143,7 +146,8 @@ def __split__(self): for node in self.nodes: spl.append(node) return spl - + + class EventLog: def __init__(self, trace=None): """ @@ -179,7 +183,7 @@ def remove_trace(self, trace, count=1): self.log[trace_tuple] -= count else: del self.log[trace_tuple] - + def get_variant_count(self, trace): """ Returns the count of the specified trace in the log. @@ -188,7 +192,7 @@ def get_variant_count(self, trace): """ trace_tuple = tuple(trace.nodes) return self.log.get(trace_tuple, 0) - + def get_most_frequent_variant(self): """ Returns the trace variant with the highest occurrence along with its count. @@ -201,7 +205,7 @@ def get_most_frequent_variant(self): # Find the trace with the maximum count max_trace_tuple = max(self.log, key=self.log.get) return Trace(list(max_trace_tuple)) - + def get_traces(self): """ Extracts and returns a list of all unique trace variants in the event log. @@ -231,7 +235,8 @@ def __iter__(self): for trace_tuple, count in sorted_log: for _ in range(count): yield Trace(list(trace_tuple)) - + + def get_sublists(lst): """ Generates all possible non-empty sublists of a list. @@ -244,6 +249,7 @@ def get_sublists(lst): sublists.extend(combinations(lst, r)) return sublists + def levenshtein_distance(seq1, seq2): """ Calculates the Levenshtein distance between two sequences. @@ -271,4 +277,4 @@ def levenshtein_distance(seq1, seq2): matrix[x][y] = min( matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 ) - return matrix[size_x - 1][size_y - 1] \ No newline at end of file + return matrix[size_x - 1][size_y - 1] From dd045d2871888787e111020a8b4e9b4044298594 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:20:42 +0200 Subject: [PATCH 42/54] Lint --- explainer/explainer.py | 4 ++-- explainer/tests/explainer_test.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index aa01c45..ff72641 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -80,7 +80,7 @@ def determine_conformance_rate(self, event_log, constraints=None): pass @abstractmethod - def determine_fitness_rate(self, event_log, constraints = None): + def determine_fitness_rate(self, event_log, constraints=None): pass @abstractmethod @@ -201,7 +201,7 @@ def get_most_frequent_variant(self): """ if not self.log: return None, 0 # Return None and 0 if the log is empty - + # Find the trace with the maximum count max_trace_tuple = max(self.log, key=self.log.get) return Trace(list(max_trace_tuple)) diff --git a/explainer/tests/explainer_test.py b/explainer/tests/explainer_test.py index 1746327..14cbeae 100644 --- a/explainer/tests/explainer_test.py +++ b/explainer/tests/explainer_test.py @@ -1,6 +1,7 @@ from explainer.explainer import * from explainer.explainer_regex import ExplainerRegex + # Test 1: Adding and checking constraints def test_add_constraint(): explainer = ExplainerRegex() @@ -14,7 +15,9 @@ def test_remove_constraint(): explainer.add_constraint("A.*B.*C") explainer.add_constraint("B.*C") explainer.remove_constraint(0) - assert "A.*B.*C" not in explainer.constraints, "Constraint 'A.*B.*C' should be removed." + assert ( + "A.*B.*C" not in explainer.constraints + ), "Constraint 'A.*B.*C' should be removed." # Test 3: Activation of constraints From 79faa02256b9e63be08c62e9097a3120fbe95599 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:26:09 +0200 Subject: [PATCH 43/54] Linter --- explainer/explainer_regex.py | 58 +++++++++++------------------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 83b084c..f6ba235 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -3,6 +3,7 @@ from itertools import combinations, product, chain from explainer import Explainer, Trace, EventLog, get_sublists, levenshtein_distance + class ExplainerRegex(Explainer): def __init__(self): super().__init__() @@ -26,7 +27,7 @@ def add_constraint(self, regex): self.constraints.append(regex) max_length = 0 for con in self.constraints: - max_length += len(con) + max_length += len(con) if self.contradiction(len(regex) + max_length): self.constraints.remove(regex) print(f"Constraint {regex} contradicts the other constraints.") @@ -128,10 +129,10 @@ def contradiction(self, max_length): for combination in product(nodes, repeat=length): test_str = "".join(combination) if all(re.search(con, test_str) for con in self.constraints): - self.adherent_trace = test_str - return False # Found a match + self.adherent_trace = test_str + return False # Found a match return True # No combination satisfied all constraints - + def contradiction_by_length(self, length): """ Checks if there is a contradiction among the constraints specifically for a given length. @@ -142,7 +143,9 @@ def contradiction_by_length(self, length): if length <= 0: return True nodes = self.get_nodes_from_constraint() - nodes = nodes + nodes # Assuming you need to double the nodes as in your previous snippet + nodes = ( + nodes + nodes + ) # Assuming you need to double the nodes as in your previous snippet for combination in product(nodes, repeat=length): test_str = "".join(combination) @@ -152,7 +155,6 @@ def contradiction_by_length(self, length): return True # No combination of this specific length satisfied all constraints - def minimal_expl(self, trace): """ Provides a minimal explanation for non-conformance, given the trace and constraints. @@ -353,7 +355,7 @@ def determine_conformance_rate(self, event_log, constraints=None): break return (len_log - non_conformant) / len_log - def determine_fitness_rate(self, event_log, constraints = None): + def determine_fitness_rate(self, event_log, constraints=None): if not self.constraints and not constraints: return "The explainer have no constraints" if constraints == None: @@ -367,9 +369,7 @@ def determine_fitness_rate(self, event_log, constraints = None): conformant += count return conformant / (len(event_log) * len(constraints)) - def variant_ctrb_to_fitness( - self, event_log, trace, constraints=None - ): + def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): if not self.constraints and not constraints: return "The explainer have no constraints" if not constraints: @@ -383,11 +383,8 @@ def variant_ctrb_to_fitness( contribution_of_trace = contribution_of_trace / len(constraints) contribution_of_trace = nr * contribution_of_trace return contribution_of_trace / total_traces - - - def variant_ctrb_to_conformance_loss( - self, event_log, trace, constraints=None - ): + + def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): """ Calculates the contribution of a specific trace to the conformance loss of the event log. @@ -405,9 +402,8 @@ def variant_ctrb_to_conformance_loss( if not self.conformant(trace, constraints= constraints): contribution_of_trace = event_log.get_variant_count(trace) - return contribution_of_trace / total_traces - + def constraint_ctrb_to_conformance(self, log, constraints, index): """Determines the Shapley value-based contribution of a constraint to a the overall conformance rate. @@ -440,12 +436,12 @@ def constraint_ctrb_to_conformance(self, log, constraints, index): ) sub_ctrbs.append(sub_ctrb) return sum(sub_ctrbs) - + def constraint_ctrb_to_fitness(self, log, constraints, index): if len(constraints) < index: raise Exception("Constraint not in constraint list.") if not self.constraints and not constraints: - return "The explainer have no constraints" + return "The explainer have no constraints" if not constraints: constraints = self.constraints contributor = constraints[index] @@ -454,7 +450,8 @@ def constraint_ctrb_to_fitness(self, log, constraints, index): if re.search(contributor, "".join(trace)): ctrb_count += count return ctrb_count / (len(log) * len(constraints)) - + + def determine_powerset(elements): """Determines the powerset of a list of elements Args: @@ -480,25 +477,4 @@ def get_iterative_subtrace(trace): sublists = [] for i in range(0, len(trace)): sublists.append(trace.nodes[0 : i + 1]) - return sublists - -exp = ExplainerRegex() - -traces = [ - Trace(['A', 'B','C']), - Trace(['A', 'B']), - Trace(['B']), - Trace(['B','C']) -] -event_log = EventLog() -event_log.add_trace(traces[0], 10) # The second parameter is how many variants you'd like to add, leave blank for 1 -event_log.add_trace(traces[1], 10) -event_log.add_trace(traces[2], 10) -event_log.add_trace(traces[3], 20) -exp.add_constraint('^A') -exp.add_constraint('C$') - -print(exp.determine_conformance_rate(event_log)) -print(exp.determine_fitness_rate(event_log)) -print(exp.constraint_ctrb_to_conformance(event_log, exp.constraints,0)) From aaf815ed54de2955827a936ba7ec17f2fe7849ff Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Tue, 30 Apr 2024 11:28:15 +0200 Subject: [PATCH 44/54] Linteer --- explainer/explainer_regex.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index f6ba235..4cce058 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -145,7 +145,7 @@ def contradiction_by_length(self, length): nodes = self.get_nodes_from_constraint() nodes = ( nodes + nodes - ) # Assuming you need to double the nodes as in your previous snippet + ) # Assuming you need to double the nodes as in your previous snippet for combination in product(nodes, repeat=length): test_str = "".join(combination) @@ -354,10 +354,10 @@ def determine_conformance_rate(self, event_log, constraints=None): non_conformant += count break return (len_log - non_conformant) / len_log - + def determine_fitness_rate(self, event_log, constraints=None): if not self.constraints and not constraints: - return "The explainer have no constraints" + return "The explainer have no constraints" if constraints == None: constraints = self.constraints if len(constraints) == 0: @@ -371,7 +371,7 @@ def determine_fitness_rate(self, event_log, constraints=None): def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): if not self.constraints and not constraints: - return "The explainer have no constraints" + return "The explainer have no constraints" if not constraints: constraints = self.constraints total_traces = len(event_log) @@ -399,8 +399,8 @@ def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): constraints = self.constraints total_traces = len(event_log) contribution_of_trace = 0 - - if not self.conformant(trace, constraints= constraints): + + if not self.conformant(trace, constraints=constraints): contribution_of_trace = event_log.get_variant_count(trace) return contribution_of_trace / total_traces From 4a3d97f2a0af8b5ac393161d595557db00b693af Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 2 May 2024 12:59:43 +0200 Subject: [PATCH 45/54] added init_.py file --- explainer/__init__.py | 0 explainer/explainer_regex.py | 2 +- {explainer/tests => tests/explainer}/explainer_test.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 explainer/__init__.py rename {explainer/tests => tests/explainer}/explainer_test.py (99%) diff --git a/explainer/__init__.py b/explainer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 4cce058..c1eedcf 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -1,7 +1,7 @@ import math import re from itertools import combinations, product, chain -from explainer import Explainer, Trace, EventLog, get_sublists, levenshtein_distance +from explainer.explainer import Explainer, Trace, EventLog, get_sublists, levenshtein_distance class ExplainerRegex(Explainer): diff --git a/explainer/tests/explainer_test.py b/tests/explainer/explainer_test.py similarity index 99% rename from explainer/tests/explainer_test.py rename to tests/explainer/explainer_test.py index 14cbeae..9d3868b 100644 --- a/explainer/tests/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -1,5 +1,5 @@ -from explainer.explainer import * from explainer.explainer_regex import ExplainerRegex +from explainer.explainer import Trace, EventLog # Test 1: Adding and checking constraints From b17cdecd601e3ebd6100f1dbf66aa1bce00eee67 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 2 May 2024 16:14:57 +0200 Subject: [PATCH 46/54] Updated tutorial --- explainer/explainer.py | 13 +- explainer/explainer_signal.py | 13 +- explainer/tutorial/explainer_tutorial_2.ipynb | 190 +++++++++++++++--- 3 files changed, 179 insertions(+), 37 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index ff72641..44697a1 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -22,17 +22,15 @@ def set_minimal_solution(self, minimal_solution): """ self.minimal_solution = minimal_solution + @abstractmethod def add_constraint(self, constr): """ Adds a new constraint and updates the nodes list. :param constr: A regular expression or Signal constrain representing the constraint. """ - self.constraints.append(constr) - if self.contradiction(): - self.constraints.remove(constr) - print(f"Constraint {constr} contradicts the other constraints.") - + pass + # Marking remove_constraint as abstract as an example @abstractmethod def remove_constraint(self, idx): @@ -69,11 +67,6 @@ def counterfactual_expl(self, trace): # Implementation remains the same pass - @abstractmethod - def evaluate_similarity(self, trace, cmp_trace=None): - # Implementation remains the same - pass - @abstractmethod def determine_conformance_rate(self, event_log, constraints=None): # Implementation remains the same diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 0bafd87..f3fb9e6 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -206,6 +206,15 @@ def activation(self, trace, constraints=None): if re.search(constraint, node): return True return False + + def add_constraint(self, constr): + """ + Adds a new constraint and updates the nodes list. + + :param constr: A regular expression or Signal constrain representing the constraint. + """ + self.constraints.append(constr) + def conformant(self, trace, constraints=None): if not constraints: @@ -304,7 +313,7 @@ def operate_on_trace(self, trace, score, explanation_path, depth=0): best_subtrace = None best_score = -float("inf") for subtrace in counter_factuals: - current_score = self.evaluate_similarity(subtrace[0].nodes) + current_score = self.evaluate_similarity(subtrace[0]) if current_score > best_score: best_score = current_score best_subtrace = subtrace[0] @@ -390,7 +399,7 @@ def evaluate_similarity(self, trace, cmp_trace=None): :return: A normalized score indicating the similarity between the adherent trace and the given trace. """ if cmp_trace == None: - cmp_trace = self.adherent_trace.nodes + cmp_trace = "".join(self.adherent_trace) trace_len = len("".join(trace)) length = len(cmp_trace) lev_distance = levenshtein_distance(cmp_trace, "".join(trace)) diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/explainer/tutorial/explainer_tutorial_2.ipynb index ee057a0..5f3cbc4 100644 --- a/explainer/tutorial/explainer_tutorial_2.ipynb +++ b/explainer/tutorial/explainer_tutorial_2.ipynb @@ -4,7 +4,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Signal" + "## Explainer for Signal\n", + "\n", + "The explainer is also made for SAP Signavio Analytics Language, a query language made for performing process mining tasks on large amount of event data.\n", + "\n", + "First, import the necessary modules" ] }, { @@ -19,6 +23,89 @@ "from explainer_signal import ExplainerSignal" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initilize the explainer\n", + "\n", + "The signal explainer is developed to be a proof of concept, hence the code is highly tailored to be used on the selected data set. With access to more API calls and conformance queries, that do not need a data set, the explainer could be developed for a broader usage." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "exp = ExplainerSignal()\n", + "exp.set_endpoint('/g/api/pi-graphql/signal') #Load the data set\n", + "\n", + "# Let's add some constrainst, these are taken from the tutorial in bpmnconstraints module\n", + "\n", + "exp.add_constraint(\"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\")\n", + "exp.add_constraint(\"(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\")\n", + "exp.add_constraint(\"(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Minimal possible trace violation\n", + "\n", + "The same fashion as ExplainerRegex." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['credit requested', 'review request', 'prepare special terms', 'assess risks', 'prepare special terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent']\n", + "---------------------\n", + "Non-conformance due to: Constraint ((^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)) is violated by subtrace: ('credit requested', 'review request')\n" + ] + } + ], + "source": [ + "trace = Trace(['credit requested', 'review request', 'prepare special terms', 'assess risks', 'prepare special terms', 'prepare contract', 'assess risks', 'prepare contract', 'send quote', 'send quote', 'quote sent'])\n", + "print(trace.nodes)\n", + "print(\"---------------------\")\n", + "print(exp.minimal_expl(trace))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Counterfactuals\n", + "\n", + "Also, similar to ExplainerRegex." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Addition (Added review request at position 0): review request->credit requested->review request->prepare special terms->assess risks->prepare special terms->prepare contract->assess risks->prepare contract->send quote->send quote->quote sent\n" + ] + } + ], + "source": [ + "print(exp.counterfactual_expl(trace))" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -30,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -43,13 +130,6 @@ } ], "source": [ - "exp = ExplainerSignal()\n", - "exp.set_endpoint('/g/api/pi-graphql/signal') #Load the data set\n", - "\n", - "exp.add_constraint(\"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\")\n", - "exp.add_constraint(\"(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\")\n", - "exp.add_constraint(\"(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\")\n", - "\n", "conf_rate = exp.determine_conformance_rate()\n", "fit_rate = exp.determine_fitness_rate()\n", "print(\"Conformance rate: \" + str(conf_rate))\n", @@ -67,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -113,7 +193,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -157,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -189,7 +269,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 7: Shapely values\n", + "## Shapely values\n", "\n", "`constraint_ctrb_to_conformance` determines how much a specific constraint contributes to the overall conformance loss. \n", "\n", @@ -198,7 +278,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -230,12 +310,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 8: Combining BPMN2CONSTRAINTS and the Explainer" + "## Step 8: Combining BPMN2CONSTRAINTS and the Explainer\n", + "\n", + "By using code from the bpmnconstraints tutorial, one can extract the constraints from a diagram and apply the explainer to that data set" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -288,9 +370,16 @@ "display(Image(png_request.content))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These are the constraints generated from the diagram" + ] + }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -304,7 +393,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 119/119 [00:00<00:00, 2365507.94it/s]" + "100%|██████████| 119/119 [00:00<00:00, 2970965.33it/s]" ] }, { @@ -324,12 +413,20 @@ ], "source": [ "signal_constraints = compile_bpmn_diagram(path, \"SIGNAL\", False)\n", - "print(signal_constraints)" + "print(signal_constraints)\n", + "print(\"The amount of constraints: \" + str(len(signal_constraints)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can use the explainer with the data set and the constraints\n" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -353,9 +450,16 @@ "print(\"Fitness rate : \" + str(fit_rate))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_conformance_loss`" + ] + }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -390,9 +494,16 @@ "print(f\"Our total conformance loss is {1 - conf_rate}\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`variant_ctrb_to_fitness`" + ] + }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -427,9 +538,16 @@ "print(f\"Our total fitness rate is {fit_rate}\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`constraint_ctrb_to_fitness`" + ] + }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -471,12 +589,34 @@ "print(f\"Our total fitness rate is {fit_rate}\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`constraint_ctrb_to_conformance`\n", + "\n", + "This does not seem to work currently, my suspicion is that the amount of constraints crashes the computer when applying the computationally heavy Shapley values to it " + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", + "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", + "\u001b[1;31mClick here for more info. \n", + "\u001b[1;31mView Jupyter log for further details." + ] + } + ], "source": [ + "\n", "\"\"\"\n", "total_distribution = 0\n", "i = 0\n", From 00bc9622a62db2efdc694f40603ae97393213952 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 2 May 2024 16:15:05 +0200 Subject: [PATCH 47/54] asd --- explainer/tutorial/explainer_tutorial_2.ipynb | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/explainer/tutorial/explainer_tutorial_2.ipynb index 5f3cbc4..4ed4896 100644 --- a/explainer/tutorial/explainer_tutorial_2.ipynb +++ b/explainer/tutorial/explainer_tutorial_2.ipynb @@ -310,7 +310,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 8: Combining BPMN2CONSTRAINTS and the Explainer\n", + "## Combining BPMN2CONSTRAINTS and the Explainer\n", "\n", "By using code from the bpmnconstraints tutorial, one can extract the constraints from a diagram and apply the explainer to that data set" ] @@ -393,14 +393,15 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 119/119 [00:00<00:00, 2970965.33it/s]" + "100%|██████████| 119/119 [00:00<00:00, 2434742.32it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "[\"(^'review request')\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('standard terms applicable'|'assess risks')*(('standard terms applicable'ANY*'assess risks'ANY*)|('assess risks'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'assess risks')*$)\", \"(^NOT('standard terms applicable'|'prepare special terms')*(('standard terms applicable'ANY*'prepare special terms'ANY*)|('prepare special terms'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'prepare special terms')*$)\", \"(^NOT('standard terms applicable'|'calculate terms')*(('standard terms applicable'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'calculate terms')*$)\", \"(^NOT('assess risks'|'prepare special terms')*(('assess risks'ANY*'prepare special terms'ANY*)|('prepare special terms'ANY* 'assess risks' ANY*))* NOT('assess risks'|'prepare special terms')*$)\", \"(^NOT('assess risks'|'calculate terms')*(('assess risks'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'assess risks' ANY*))* NOT('assess risks'|'calculate terms')*$)\", \"(^NOT('prepare special terms'|'calculate terms')*(('prepare special terms'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'calculate terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'assess risks'))*$)\", \"(^NOT('assess risks')*('review request' NOT('assess risks')*'assess risks'NOT('assess risks')*)*NOT('assess risks')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'prepare special terms'))*$)\", \"(^NOT('prepare special terms')*('review request' NOT('prepare special terms')*'prepare special terms'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'calculate terms'))*$)\", \"(^NOT('calculate terms')*('review request' NOT('calculate terms')*'calculate terms'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'prepare special terms'))*$)\", \"(^NOT('prepare special terms')*('review request' NOT('prepare special terms')*'prepare special terms'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'calculate terms'))*$)\", \"(^NOT('calculate terms')*('review request' NOT('calculate terms')*'calculate terms'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^(((NOT('prepare special terms')*) ('calculate terms' NOT('prepare special terms')*)*)|((NOT('calculate terms')*)('prepare special terms' NOT('calculate terms')*)*))$)\", \"(('prepare special terms'|'calculate terms'))\", \"(^NOT('calculate terms'|'prepare contract')*('calculate terms'~>'prepare contract')*NOT('calculate terms'|'prepare contract')*$)\", \"(^NOT('calculate terms'|'prepare contract')*(('calculate terms'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'calculate terms' ANY*))* NOT('calculate terms'|'prepare contract')*$)\", \"( ^ NOT('calculate terms'|'prepare contract')* ('calculate terms'NOT('calculate terms'|'prepare contract')*'prepare contract'NOT('calculate terms'|'prepare contract')*)*NOT('calculate terms'|'prepare contract')* $)\", \"(^NOT('calculate terms'|'send quote')*('calculate terms'~>'send quote')*NOT('calculate terms'|'send quote')*$)\", \"(^NOT('calculate terms'|'send quote')*(('calculate terms'ANY*'send quote'ANY*)|('send quote'ANY* 'calculate terms' ANY*))* NOT('calculate terms'|'send quote')*$)\", \"( ^ NOT('calculate terms'|'send quote')* ('calculate terms'NOT('calculate terms'|'send quote')*'send quote'NOT('calculate terms'|'send quote')*)*NOT('calculate terms'|'send quote')* $)\", \"(^NOT('prepare contract'|'send quote')*('prepare contract'~>'send quote')*NOT('prepare contract'|'send quote')*$)\", \"(^NOT('prepare contract'|'send quote')*(('prepare contract'ANY*'send quote'ANY*)|('send quote'ANY* 'prepare contract' ANY*))* NOT('prepare contract'|'send quote')*$)\", \"(('prepare contract'|'send quote'))\", \"( ^ NOT('prepare contract'|'send quote')* ('prepare contract'NOT('prepare contract'|'send quote')*'send quote'NOT('prepare contract'|'send quote')*)*NOT('prepare contract'|'send quote')* $)\", \"(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\", \"(^NOT('prepare contract')*('prepare contract'NOT('prepare contract')*'send quote'NOT('prepare contract')*)*NOT('prepare contract')*$)\", \"(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\", \"(^NOT('assess risks')*('assess risks'NOT('assess risks')*'send quote'NOT('assess risks')*)*NOT('assess risks')*$)\", \"('send quote'$)\", \"(^NOT('prepare special terms'|'prepare contract')*('prepare special terms'~>'prepare contract')*NOT('prepare special terms'|'prepare contract')*$)\", \"(^NOT('prepare special terms'|'prepare contract')*(('prepare special terms'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'prepare contract')*$)\", \"( ^ NOT('prepare special terms'|'prepare contract')* ('prepare special terms'NOT('prepare special terms'|'prepare contract')*'prepare contract'NOT('prepare special terms'|'prepare contract')*)*NOT('prepare special terms'|'prepare contract')* $)\", \"(^NOT('prepare special terms'|'send quote')*('prepare special terms'~>'send quote')*NOT('prepare special terms'|'send quote')*$)\", \"(^NOT('prepare special terms'|'send quote')*(('prepare special terms'ANY*'send quote'ANY*)|('send quote'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'send quote')*$)\", \"( ^ NOT('prepare special terms'|'send quote')* ('prepare special terms'NOT('prepare special terms'|'send quote')*'send quote'NOT('prepare special terms'|'send quote')*)*NOT('prepare special terms'|'send quote')* $)\", \"(^NOT('assess risks'|'send quote')*('assess risks'~>'send quote')*NOT('assess risks'|'send quote')*$)\", \"(^NOT('assess risks'|'send quote')*(('assess risks'ANY*'send quote'ANY*)|('send quote'ANY* 'assess risks' ANY*))* NOT('assess risks'|'send quote')*$)\", \"( ^ NOT('assess risks'|'send quote')* ('assess risks'NOT('assess risks'|'send quote')*'send quote'NOT('assess risks'|'send quote')*)*NOT('assess risks'|'send quote')* $)\", \"(^NOT('calculate terms')* ('calculate terms' ANY*'prepare contract')* NOT('calculate terms')*$)\", \"(^NOT('calculate terms')*('calculate terms'NOT('calculate terms')*'prepare contract'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^NOT('prepare special terms')* ('prepare special terms' ANY*'prepare contract')* NOT('prepare special terms')*$)\", \"(^NOT('prepare special terms')*('prepare special terms'NOT('prepare special terms')*'prepare contract'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\"]\n" + "[\"(^'review request')\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'prepare contract')*('review request'~>'prepare contract')*NOT('review request'|'prepare contract')*$)\", \"(^NOT('review request'|'prepare contract')*(('review request'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'review request' ANY*))* NOT('review request'|'prepare contract')*$)\", \"(('review request'|'prepare contract'))\", \"( ^ NOT('review request'|'prepare contract')* ('review request'NOT('review request'|'prepare contract')*'prepare contract'NOT('review request'|'prepare contract')*)*NOT('review request'|'prepare contract')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('review request'|'send quote')*('review request'~>'send quote')*NOT('review request'|'send quote')*$)\", \"(^NOT('review request'|'send quote')*(('review request'ANY*'send quote'ANY*)|('send quote'ANY* 'review request' ANY*))* NOT('review request'|'send quote')*$)\", \"(('review request'|'send quote'))\", \"( ^ NOT('review request'|'send quote')* ('review request'NOT('review request'|'send quote')*'send quote'NOT('review request'|'send quote')*)*NOT('review request'|'send quote')* $)\", \"(^NOT('standard terms applicable'|'assess risks')*(('standard terms applicable'ANY*'assess risks'ANY*)|('assess risks'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'assess risks')*$)\", \"(^NOT('standard terms applicable'|'prepare special terms')*(('standard terms applicable'ANY*'prepare special terms'ANY*)|('prepare special terms'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'prepare special terms')*$)\", \"(^NOT('standard terms applicable'|'calculate terms')*(('standard terms applicable'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'standard terms applicable' ANY*))* NOT('standard terms applicable'|'calculate terms')*$)\", \"(^NOT('assess risks'|'prepare special terms')*(('assess risks'ANY*'prepare special terms'ANY*)|('prepare special terms'ANY* 'assess risks' ANY*))* NOT('assess risks'|'prepare special terms')*$)\", \"(^NOT('assess risks'|'calculate terms')*(('assess risks'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'assess risks' ANY*))* NOT('assess risks'|'calculate terms')*$)\", \"(^NOT('prepare special terms'|'calculate terms')*(('prepare special terms'ANY*'calculate terms'ANY*)|('calculate terms'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'calculate terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'assess risks'))*$)\", \"(^NOT('assess risks')*('review request' NOT('assess risks')*'assess risks'NOT('assess risks')*)*NOT('assess risks')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'prepare special terms'))*$)\", \"(^NOT('prepare special terms')*('review request' NOT('prepare special terms')*'prepare special terms'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'calculate terms'))*$)\", \"(^NOT('calculate terms')*('review request' NOT('calculate terms')*'calculate terms'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'prepare special terms'))*$)\", \"(^NOT('prepare special terms')*('review request' NOT('prepare special terms')*'prepare special terms'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\", \"(^ (NOT 'review request' | ('review request' (NOT 'review request')* 'calculate terms'))*$)\", \"(^NOT('calculate terms')*('review request' NOT('calculate terms')*'calculate terms'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^(((NOT('prepare special terms')*) ('calculate terms' NOT('prepare special terms')*)*)|((NOT('calculate terms')*)('prepare special terms' NOT('calculate terms')*)*))$)\", \"(('prepare special terms'|'calculate terms'))\", \"(^NOT('calculate terms'|'prepare contract')*('calculate terms'~>'prepare contract')*NOT('calculate terms'|'prepare contract')*$)\", \"(^NOT('calculate terms'|'prepare contract')*(('calculate terms'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'calculate terms' ANY*))* NOT('calculate terms'|'prepare contract')*$)\", \"( ^ NOT('calculate terms'|'prepare contract')* ('calculate terms'NOT('calculate terms'|'prepare contract')*'prepare contract'NOT('calculate terms'|'prepare contract')*)*NOT('calculate terms'|'prepare contract')* $)\", \"(^NOT('calculate terms'|'send quote')*('calculate terms'~>'send quote')*NOT('calculate terms'|'send quote')*$)\", \"(^NOT('calculate terms'|'send quote')*(('calculate terms'ANY*'send quote'ANY*)|('send quote'ANY* 'calculate terms' ANY*))* NOT('calculate terms'|'send quote')*$)\", \"( ^ NOT('calculate terms'|'send quote')* ('calculate terms'NOT('calculate terms'|'send quote')*'send quote'NOT('calculate terms'|'send quote')*)*NOT('calculate terms'|'send quote')* $)\", \"(^NOT('prepare contract'|'send quote')*('prepare contract'~>'send quote')*NOT('prepare contract'|'send quote')*$)\", \"(^NOT('prepare contract'|'send quote')*(('prepare contract'ANY*'send quote'ANY*)|('send quote'ANY* 'prepare contract' ANY*))* NOT('prepare contract'|'send quote')*$)\", \"(('prepare contract'|'send quote'))\", \"( ^ NOT('prepare contract'|'send quote')* ('prepare contract'NOT('prepare contract'|'send quote')*'send quote'NOT('prepare contract'|'send quote')*)*NOT('prepare contract'|'send quote')* $)\", \"(^NOT('prepare contract')* ('prepare contract' ANY*'send quote')* NOT('prepare contract')*$)\", \"(^NOT('prepare contract')*('prepare contract'NOT('prepare contract')*'send quote'NOT('prepare contract')*)*NOT('prepare contract')*$)\", \"(^NOT('assess risks')* ('assess risks' ANY*'send quote')* NOT('assess risks')*$)\", \"(^NOT('assess risks')*('assess risks'NOT('assess risks')*'send quote'NOT('assess risks')*)*NOT('assess risks')*$)\", \"('send quote'$)\", \"(^NOT('prepare special terms'|'prepare contract')*('prepare special terms'~>'prepare contract')*NOT('prepare special terms'|'prepare contract')*$)\", \"(^NOT('prepare special terms'|'prepare contract')*(('prepare special terms'ANY*'prepare contract'ANY*)|('prepare contract'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'prepare contract')*$)\", \"( ^ NOT('prepare special terms'|'prepare contract')* ('prepare special terms'NOT('prepare special terms'|'prepare contract')*'prepare contract'NOT('prepare special terms'|'prepare contract')*)*NOT('prepare special terms'|'prepare contract')* $)\", \"(^NOT('prepare special terms'|'send quote')*('prepare special terms'~>'send quote')*NOT('prepare special terms'|'send quote')*$)\", \"(^NOT('prepare special terms'|'send quote')*(('prepare special terms'ANY*'send quote'ANY*)|('send quote'ANY* 'prepare special terms' ANY*))* NOT('prepare special terms'|'send quote')*$)\", \"( ^ NOT('prepare special terms'|'send quote')* ('prepare special terms'NOT('prepare special terms'|'send quote')*'send quote'NOT('prepare special terms'|'send quote')*)*NOT('prepare special terms'|'send quote')* $)\", \"(^NOT('assess risks'|'send quote')*('assess risks'~>'send quote')*NOT('assess risks'|'send quote')*$)\", \"(^NOT('assess risks'|'send quote')*(('assess risks'ANY*'send quote'ANY*)|('send quote'ANY* 'assess risks' ANY*))* NOT('assess risks'|'send quote')*$)\", \"( ^ NOT('assess risks'|'send quote')* ('assess risks'NOT('assess risks'|'send quote')*'send quote'NOT('assess risks'|'send quote')*)*NOT('assess risks'|'send quote')* $)\", \"(^NOT('calculate terms')* ('calculate terms' ANY*'prepare contract')* NOT('calculate terms')*$)\", \"(^NOT('calculate terms')*('calculate terms'NOT('calculate terms')*'prepare contract'NOT('calculate terms')*)*NOT('calculate terms')*$)\", \"(^NOT('prepare special terms')* ('prepare special terms' ANY*'prepare contract')* NOT('prepare special terms')*$)\", \"(^NOT('prepare special terms')*('prepare special terms'NOT('prepare special terms')*'prepare contract'NOT('prepare special terms')*)*NOT('prepare special terms')*$)\"]\n", + "The amount of constraints: 119\n" ] }, { @@ -604,15 +605,14 @@ "metadata": {}, "outputs": [ { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", - "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", - "\u001b[1;31mClick here for more info. \n", - "\u001b[1;31mView Jupyter log for further details." - ] + "data": { + "text/plain": [ + "'\\ntotal_distribution = 0\\ni = 0\\nfor con in signal_constraints:\\n ctrb = exp.constraint_ctrb_to_conformance(\\n log=exp.event_log,\\n constraints=exp.constraints,\\n index=i\\n )\\n i +=1\\n total_distribution += ctrb\\n # Let\\'s just show some traces\\n if i % 10 == 0:\\n print(f\"Contribution is: {ctrb}, for constraint {con}\")\\n \\nprint(f\"Total distribution to the conformance loss is {total_distribution}\")\\nprint(f\"Our total conformance loss is { 1 - conf_rate}\")\\n'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ From 1b60d24a7fb6eeab348773bf37224f75c3ae6e5f Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 2 May 2024 16:35:28 +0200 Subject: [PATCH 48/54] Updated readme --- explainer/README.md | 48 ++++--------------- explainer/tutorial/explainer_tutorial_2.ipynb | 4 +- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/explainer/README.md b/explainer/README.md index e4052a2..391e96d 100644 --- a/explainer/README.md +++ b/explainer/README.md @@ -1,47 +1,15 @@ # Symbolic Explanations of Process Conformance Violations -## Introduction +This module is made by Marcus Rost, for his thesis subject, **Symbolic Explanations for Control-Flow Objects**. +The code should work properly, with some hard-coded parts that requires the data set for the signal code to function. This module is more seen as a proof of concept, where some core functions from explainability theory is utilized. -# Regex usage for the first iteration of the software +## Overview +The module mainly consists of 2 classes, ExplainerRegex and ExplainerSignal. -## 1. Sequence Constraint -Pattern: `'A.*B.*C'` +**ExplainerRegex** is meant to simulate a real event log and system, with traces and constraints, using regex patterns. -Explanation: This regex specifies that for a trace to be conformant, it must contain the nodes 'A', 'B', and 'C' in that order, though not necessarily consecutively. The .* allows for any number of intervening nodes between the specified nodes. +**ExplainerSignal** uses real event logs to calculate the explanations. This uses SAP Signavio's API to do so, but should in reality have it's own interpreter for the Signal queries, instead of having to make API calls. -> Example: A trace ['A', 'X', 'B', 'Y', 'C'] would be conformant, while ['A', 'C', 'B'] would not. +Currently, the Signal code is sort of bad and specialized for the specific data set used. Hopefully, someone can see the value of what it done, and generalize it further. -## 2. Immediate Succession -Pattern: `'AB'` - -Explanation: This regex specifies that node 'A' must be immediately followed by node 'B' with no intervening nodes. - -> Example: A trace ['A', 'B', 'C'] would be conformant, while ['A', 'X', 'B'] would not. - -## 3. Optional Node -Pattern: `'A(B)?C'` - -Explanation: This regex specifies that the node 'B' is optional between 'A' and 'C'. The node 'C' must follow 'A', but 'B' can either be present or absent. - -> Example: Both traces ['A', 'B', 'C'] and ['A', 'C'] would be conformant. - -## 4. Excluding Specific Nodes -Pattern: `'A[^D]*B'` - -Explanation: This regex specifies that 'A' must be followed by 'B' without the occurrence of 'D' in between them. The [^D] part matches any character except 'D'. - -> Example: A trace ['A', 'C', 'B'] would be conformant, while ['A', 'D', 'B'] would not. - -## 5. Repetition of Nodes -Pattern: `'A(B{2,3})C'` - -Explanation: This regex specifies that 'A' must be followed by 'B' repeated 2 to 3 times and then followed by 'C'. - -> Example: Traces ['A', 'B', 'B', 'C'] and ['A', 'B', 'B', 'B', 'C'] would be conformant, while ['A', 'B', 'C'] or ['A', 'B', 'B', 'B', 'B', 'C'] would not. - -## 6. Alternative Paths -Pattern: `'A(B|D)C'` - -Explanation: This regex specifies that after 'A', there must be either a 'B' or a 'D', followed by a 'C'. - -> Example: Both traces ['A', 'B', 'C'] and ['A', 'D', 'C'] would be conformant. +There is also 2 Notebook's located in `explainer/tutorial`, but I recommend doing `bpmn2constraints/tutorial/tutorial.ipynb` first, to see how to do the configuration for the API works. \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/explainer/tutorial/explainer_tutorial_2.ipynb index 4ed4896..a31396c 100644 --- a/explainer/tutorial/explainer_tutorial_2.ipynb +++ b/explainer/tutorial/explainer_tutorial_2.ipynb @@ -8,7 +8,7 @@ "\n", "The explainer is also made for SAP Signavio Analytics Language, a query language made for performing process mining tasks on large amount of event data.\n", "\n", - "First, import the necessary modules" + "First, import the necessary modules." ] }, { @@ -112,7 +112,7 @@ "source": [ "## Contribution functions\n", "\n", - "We'll start with conformance rate and fitness rate, given some arbritary constraints" + "We'll start with conformance rate and fitness rate, given some arbritary constraints." ] }, { From 97ee8806169e0983b8d2903566c579040f6f677f Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Thu, 2 May 2024 16:38:37 +0200 Subject: [PATCH 49/54] Lint --- explainer/explainer.py | 2 +- explainer/explainer_regex.py | 7 ++++++- explainer/explainer_signal.py | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/explainer/explainer.py b/explainer/explainer.py index 44697a1..3a3b8c0 100644 --- a/explainer/explainer.py +++ b/explainer/explainer.py @@ -30,7 +30,7 @@ def add_constraint(self, constr): :param constr: A regular expression or Signal constrain representing the constraint. """ pass - + # Marking remove_constraint as abstract as an example @abstractmethod def remove_constraint(self, idx): diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index c1eedcf..7f6bca7 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -1,7 +1,12 @@ import math import re from itertools import combinations, product, chain -from explainer.explainer import Explainer, Trace, EventLog, get_sublists, levenshtein_distance +from explainer.explainer import ( + Explainer, + Trace, + get_sublists, + levenshtein_distance, +) class ExplainerRegex(Explainer): diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index f3fb9e6..3a7d2fd 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -206,7 +206,7 @@ def activation(self, trace, constraints=None): if re.search(constraint, node): return True return False - + def add_constraint(self, constr): """ Adds a new constraint and updates the nodes list. @@ -214,7 +214,6 @@ def add_constraint(self, constr): :param constr: A regular expression or Signal constrain representing the constraint. """ self.constraints.append(constr) - def conformant(self, trace, constraints=None): if not constraints: From 129bd466d9e50c56c3c53b9b817a4cf541305123 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 6 May 2024 17:52:30 +0200 Subject: [PATCH 50/54] Reformat code --- explainer/explainer_regex.py | 10 +- explainer/explainer_signal.py | 146 ++++++++---------- explainer/{explainer.py => explainer_util.py} | 98 +----------- explainer/tutorial/explainer_tutorial_1.ipynb | 81 ++++++---- explainer/tutorial/explainer_tutorial_2.ipynb | 48 ++++-- 5 files changed, 156 insertions(+), 227 deletions(-) rename explainer/{explainer.py => explainer_util.py} (64%) diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 7f6bca7..18534a2 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -1,17 +1,19 @@ import math import re from itertools import combinations, product, chain -from explainer.explainer import ( - Explainer, +from explainer_util import ( Trace, get_sublists, levenshtein_distance, ) -class ExplainerRegex(Explainer): +class ExplainerRegex(): def __init__(self): - super().__init__() + self.constraints = [] # List to store constraints (constraint patterns) + self.adherent_trace = None + self.adherent_traces = [] + self.minimal_solution = False def set_minimal_solution(self, minimal_solution): """ diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index 3a7d2fd..ce45de8 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -1,9 +1,7 @@ -from abc import ABC, abstractmethod -from explainer import Explainer, Trace, EventLog, get_sublists, levenshtein_distance +from explainer_util import * import re import math import SignavioAuthenticator -import json from itertools import combinations, chain import requests from conf import system_instance, workspace_id, user_name, pw @@ -159,9 +157,12 @@ ] -class ExplainerSignal(Explainer): +class ExplainerSignal(): def __init__(self): - super().__init__() + self.constraints = [] # List to store constraints (constraint patterns) + self.adherent_trace = None + self.adherent_traces = [] + self.minimal_solution = False self.authenticator = SignavioAuthenticator.SignavioAuthenticator( system_instance, workspace_id, user_name, pw ) @@ -216,40 +217,7 @@ def add_constraint(self, constr): self.constraints.append(constr) def conformant(self, trace, constraints=None): - if not constraints: - constraints = self.constraints - if len(constraints) > 1: - constraints = " AND ".join( - [f"event_name MATCHES {constraint}" for constraint in constraints] - ) - else: - constraints = "".join(f"event_name MATCHES {constraints[0]}") - query = ( - f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' - ) - conformant = False - cache_key = hash(query) - if cache_key in self.cache: - for res in self.cache[cache_key]: - if trace.nodes == res[0]: - conformant = True - break - return conformant - - query_request = requests.post( - self.signal_endpoint, - cookies=self.cookies, - headers=self.headers, - json={"query": query}, - ) - result = query_request.json()["data"] - self.cache[cache_key] = result # Store the result in cache - - for res in result: - if trace.nodes == res[0]: - conformant = True - break - return conformant + return self.post_query_trace_in_dataset(trace, constraints) def minimal_expl(self, trace): @@ -272,24 +240,7 @@ def minimal_expl(self, trace): return "Trace is non-conformant, but the specific constraint violation could not be determined." def counterfactual_expl(self, trace): - constraints = self.constraints - if len(constraints) > 1: - constraints = " AND ".join( - [f"event_name MATCHES {constraint}" for constraint in constraints] - ) - else: - constraints = "".join(f"event_name MATCHES {constraints[0]}") - query = ( - f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' - ) - query_request = requests.post( - self.signal_endpoint, - cookies=self.cookies, - headers=self.headers, - json={"query": query}, - ) - result = query_request.json()["data"] - + result = self.get_all_conformant_traces() best_score = -float("inf") for res in result: current_score = self.evaluate_similarity(trace, "".join(res[0])) @@ -492,7 +443,6 @@ def constraint_ctrb_to_conformance(self, log=None, constraints=None, index=-1): return sum(sub_ctrbs) def constraint_ctrb_to_fitness(self, log=None, constraints=None, index=-1): - # Implementation remains the same if len(constraints) < index: raise Exception("Constraint not in constraint list.") if not constraints: @@ -506,53 +456,89 @@ def constraint_ctrb_to_fitness(self, log=None, constraints=None, index=-1): def check_conformance(self, constraint): query = f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE event_name MATCHES{constraint}' + return self.post_query(query) + + def check_violations(self, constraints): + combined_constraints = " OR ".join( + [f"NOT event_name MATCHES {constraint}" for constraint in constraints] + ) + query = ( + f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' + ) + return self.post_query(query) + + def get_total_cases(self): + query = 'SELECT COUNT(CASE_ID) FROM "defaultview-4"' + return self.post_query(query) + + def post_query(self, query): cache_key = hash(query) if cache_key in self.cache: return self.cache[cache_key] - query_request = requests.post( + request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, json={"query": query}, ) - result = query_request.json()["data"][0][0] + result = request.json()["data"][0][0] self.cache[cache_key] = result return result - def check_violations(self, constraints): - combined_constraints = " OR ".join( - [f"NOT event_name MATCHES {constraint}" for constraint in constraints] - ) + def post_query_trace_in_dataset(self, trace, constraints): + if not constraints: + constraints = self.constraints + if len(constraints) > 1: + constraints = " AND ".join( + [f"event_name MATCHES {constraint}" for constraint in constraints] + ) + else: + constraints = "".join(f"event_name MATCHES {constraints[0]}") query = ( - f'SELECT COUNT(CASE_ID) FROM "defaultview-4" WHERE {combined_constraints}' + f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' ) + conformant = False cache_key = hash(query) if cache_key in self.cache: - return self.cache[cache_key] + for res in self.cache[cache_key]: + if trace.nodes == res[0]: + conformant = True + break + return conformant + query_request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, json={"query": query}, ) - result = query_request.json()["data"][0][0] - self.cache[cache_key] = result - return result + result = query_request.json()["data"] + self.cache[cache_key] = result # Store the result in cache - def get_total_cases(self): - query = 'SELECT COUNT(CASE_ID) FROM "defaultview-4"' - cache_key = hash(query) - if cache_key in self.cache: - return self.cache[cache_key] - count_request = requests.post( + for res in result: + if trace.nodes == res[0]: + conformant = True + break + return conformant + + def get_all_conformant_traces(self): + constraints = self.constraints + if len(constraints) > 1: + constraints = " AND ".join( + [f"event_name MATCHES {constraint}" for constraint in constraints] + ) + else: + constraints = "".join(f"event_name MATCHES {constraints[0]}") + query = ( + f'SELECT ACTIVITY, COUNT(CASE_ID) FROM "defaultview-4" WHERE {constraints}' + ) + query_request = requests.post( self.signal_endpoint, cookies=self.cookies, headers=self.headers, json={"query": query}, ) - case_count = count_request.json()["data"][0][0] - self.cache[cache_key] = case_count - return case_count + return query_request.json()["data"] def load_variants(self): query = 'SELECT Activity From "defaultview-4"' @@ -568,10 +554,6 @@ def load_variants(self): self.event_log.add_trace(Trace(activity[0])) -def get_event_log(): - return f'Select * FROM "defaultview-4"' - - def determine_powerset(elements): """Determines the powerset of a list of elements Args: diff --git a/explainer/explainer.py b/explainer/explainer_util.py similarity index 64% rename from explainer/explainer.py rename to explainer/explainer_util.py index 3a3b8c0..e33b10e 100644 --- a/explainer/explainer.py +++ b/explainer/explainer_util.py @@ -1,101 +1,5 @@ -from abc import ABC, abstractmethod from itertools import combinations - -class Explainer(ABC): - def __init__(self): - """ - Initializes an Explainer instance. - """ - self.constraints = [] # List to store constraints (constraint patterns) - self.adherent_trace = None - self.adherent_traces = [] - self.minimal_solution = False - - def set_minimal_solution(self, minimal_solution): - """ - Tells the explainer to generate minimal solutions - Note: This will increase computations significantly - - Args: - minimal_solution (bool): True to generate minimal solutions, False if it should be the first possible - """ - self.minimal_solution = minimal_solution - - @abstractmethod - def add_constraint(self, constr): - """ - Adds a new constraint and updates the nodes list. - - :param constr: A regular expression or Signal constrain representing the constraint. - """ - pass - - # Marking remove_constraint as abstract as an example - @abstractmethod - def remove_constraint(self, idx): - """ - Removes a constraint by index and updates the nodes list if necessary. - - :param idx: Index of the constraint to be removed. - """ - pass - - # Marking activation as abstract as an example - @abstractmethod - def activation(self, trace, constraints=None): - """ - Checks if any of the nodes in the trace activates any constraint. - - :param trace: A Trace instance. - :return: Boolean indicating if any constraint is activated. - """ - pass - - @abstractmethod - def conformant(self, trace, constraints=None): - # Implementation remains the same - pass - - @abstractmethod - def minimal_expl(self, trace): - # Implementation remains the same - pass - - @abstractmethod - def counterfactual_expl(self, trace): - # Implementation remains the same - pass - - @abstractmethod - def determine_conformance_rate(self, event_log, constraints=None): - # Implementation remains the same - pass - - @abstractmethod - def determine_fitness_rate(self, event_log, constraints=None): - pass - - @abstractmethod - def variant_ctrb_to_conformance_loss(self, event_log, trace, constraints=None): - # Implementation remains the same - pass - - @abstractmethod - def variant_ctrb_to_fitness(self, event_log, trace, constraints=None): - # Implementation remains the same - pass - - @abstractmethod - def constraint_ctrb_to_conformance(self, log, constraints, index): - pass - - @abstractmethod - def constraint_ctrb_to_fitness(self, log, constraints, index): - # Implementation remains the same - pass - - class Trace: def __init__(self, nodes): """ @@ -270,4 +174,4 @@ def levenshtein_distance(seq1, seq2): matrix[x][y] = min( matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 ) - return matrix[size_x - 1][size_y - 1] + return matrix[size_x - 1][size_y - 1] \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial_1.ipynb b/explainer/tutorial/explainer_tutorial_1.ipynb index 62f0699..5fee02d 100644 --- a/explainer/tutorial/explainer_tutorial_1.ipynb +++ b/explainer/tutorial/explainer_tutorial_1.ipynb @@ -20,13 +20,15 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": {}, + "execution_count": 4, + "metadata": { + "metadata": {} + }, "outputs": [], "source": [ "import sys\n", "sys.path.append('../')\n", - "from explainer import Explainer, Trace, EventLog\n", + "from explainer_util import Trace, EventLog\n", "from explainer_regex import ExplainerRegex\n" ] }, @@ -40,8 +42,10 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, + "execution_count": 5, + "metadata": { + "metadata": {} + }, "outputs": [], "source": [ "explainer = ExplainerRegex()\n", @@ -59,8 +63,10 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, + "execution_count": 6, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -87,8 +93,10 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, + "execution_count": 7, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -204,8 +212,10 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, + "execution_count": 8, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -221,18 +231,15 @@ "--------------------------------\n", "5\n", "\n", - "Addition (Added B at position 3): A->B->A->B->C->B\n", - "Subtraction (Removed B from position 5): A->B->A->B->C\n", + "Addition (Added C at position 5): A->B->A->C->B->C\n", "\n", "Example without minimal solution\n", "--------------------------------\n", "\n", - "Addition (Added A at position 1): C->A->B->A\n", - "Addition (Added A at position 1): C->A->A->B->A\n", - "Addition (Added A at position 1): C->A->A->A->B->A\n", - "Subtraction (Removed C from position 0): A->A->A->B->A\n", - "Addition (Added C at position 4): A->A->A->B->C->A\n", - "Subtraction (Removed A from position 5): A->A->A->B->C\n", + "Addition (Added C at position 1): C->C->B->A\n", + "Addition (Added A at position 0): A->C->C->B->A\n", + "Addition (Added C at position 4): A->C->C->B->C->A\n", + "Subtraction (Removed A from position 5): A->C->C->B->C\n", "\n", "Example with minimal solution\n", "--------------------------------\n", @@ -288,8 +295,10 @@ }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, + "execution_count": 9, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -331,8 +340,10 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, + "execution_count": 10, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -365,8 +376,10 @@ }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, + "execution_count": 11, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -403,8 +416,10 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, + "execution_count": 12, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -436,8 +451,10 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, + "execution_count": 13, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -460,8 +477,10 @@ }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, + "execution_count": 14, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/explainer/tutorial/explainer_tutorial_2.ipynb index a31396c..1b8a990 100644 --- a/explainer/tutorial/explainer_tutorial_2.ipynb +++ b/explainer/tutorial/explainer_tutorial_2.ipynb @@ -14,12 +14,14 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [], "source": [ "import sys\n", "sys.path.append('../')\n", - "from explainer import Explainer, Trace, EventLog\n", + "from explainer_util import Trace\n", "from explainer_signal import ExplainerSignal" ] }, @@ -60,7 +62,9 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -91,7 +95,9 @@ { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -118,7 +124,9 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -194,7 +202,9 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -279,7 +289,9 @@ { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -318,7 +330,9 @@ { "cell_type": "code", "execution_count": 10, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "data": { @@ -380,7 +394,9 @@ { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -393,7 +409,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 119/119 [00:00<00:00, 2434742.32it/s]" + "100%|██████████| 119/119 [00:00<00:00, 2343296.60it/s]" ] }, { @@ -428,7 +444,9 @@ { "cell_type": "code", "execution_count": 12, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -461,7 +479,9 @@ { "cell_type": "code", "execution_count": 13, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", @@ -505,7 +525,9 @@ { "cell_type": "code", "execution_count": 14, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "name": "stdout", From 564bbcb583e352a4203fcd9dbd2fa511b97bbada Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 6 May 2024 17:53:55 +0200 Subject: [PATCH 51/54] Reformat --- explainer/explainer_regex.py | 2 +- explainer/explainer_signal.py | 2 +- explainer/explainer_util.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index 18534a2..a7100e1 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -8,7 +8,7 @@ ) -class ExplainerRegex(): +class ExplainerRegex: def __init__(self): self.constraints = [] # List to store constraints (constraint patterns) self.adherent_trace = None diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index ce45de8..eff8a85 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -157,7 +157,7 @@ ] -class ExplainerSignal(): +class ExplainerSignal: def __init__(self): self.constraints = [] # List to store constraints (constraint patterns) self.adherent_trace = None diff --git a/explainer/explainer_util.py b/explainer/explainer_util.py index e33b10e..a89c425 100644 --- a/explainer/explainer_util.py +++ b/explainer/explainer_util.py @@ -1,5 +1,6 @@ from itertools import combinations + class Trace: def __init__(self, nodes): """ From 7dede08ada0c312778cea22f5d88c5f47f9fbfe9 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 6 May 2024 17:54:40 +0200 Subject: [PATCH 52/54] Linter --- explainer/explainer_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/explainer/explainer_util.py b/explainer/explainer_util.py index a89c425..1b1f2fe 100644 --- a/explainer/explainer_util.py +++ b/explainer/explainer_util.py @@ -175,4 +175,4 @@ def levenshtein_distance(seq1, seq2): matrix[x][y] = min( matrix[x - 1][y] + 1, matrix[x][y - 1] + 1, matrix[x - 1][y - 1] + 1 ) - return matrix[size_x - 1][size_y - 1] \ No newline at end of file + return matrix[size_x - 1][size_y - 1] From 3cd3e0972b5da715e4909ebc47624943c656e07e Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 6 May 2024 18:14:23 +0200 Subject: [PATCH 53/54] Changed test wording --- explainer/tutorial/explainer_tutorial_2.ipynb | 6 ++++-- tests/explainer/explainer_test.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/explainer/tutorial/explainer_tutorial_2.ipynb index 1b8a990..2d0ef76 100644 --- a/explainer/tutorial/explainer_tutorial_2.ipynb +++ b/explainer/tutorial/explainer_tutorial_2.ipynb @@ -409,7 +409,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 119/119 [00:00<00:00, 2343296.60it/s]" + "100%|██████████| 119/119 [00:00<00:00, 2533615.11it/s]" ] }, { @@ -624,7 +624,9 @@ { "cell_type": "code", "execution_count": 16, - "metadata": {}, + "metadata": { + "metadata": {} + }, "outputs": [ { "data": { diff --git a/tests/explainer/explainer_test.py b/tests/explainer/explainer_test.py index 9d3868b..db931bc 100644 --- a/tests/explainer/explainer_test.py +++ b/tests/explainer/explainer_test.py @@ -1,5 +1,5 @@ from explainer.explainer_regex import ExplainerRegex -from explainer.explainer import Trace, EventLog +from explainer.explainer_util import Trace, EventLog # Test 1: Adding and checking constraints From a3c07ee351c9a8e2b1a36e57cebe6eace35aea21 Mon Sep 17 00:00:00 2001 From: MarcusRostSAP Date: Mon, 6 May 2024 18:23:00 +0200 Subject: [PATCH 54/54] Changed file locations --- .gitignore | 2 +- explainer/SignavioAuthenticator.py | 42 ------------------- explainer/explainer_regex.py | 2 +- explainer/explainer_signal.py | 8 ++-- explainer/tutorial/diagram.json | 1 - .../explainer_tutorial_1.ipynb | 39 +++++++++-------- .../explainer_tutorial_2.ipynb | 6 +-- tutorial/tutorial.ipynb | 2 +- 8 files changed, 31 insertions(+), 71 deletions(-) delete mode 100644 explainer/SignavioAuthenticator.py delete mode 100644 explainer/tutorial/diagram.json rename {explainer/tutorial => tutorial}/explainer_tutorial_1.ipynb (96%) rename {explainer/tutorial => tutorial}/explainer_tutorial_2.ipynb (99%) diff --git a/.gitignore b/.gitignore index a5dc1ba..9e10f15 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,4 @@ dmypy.json # explainer Stuff explainer/test.py explainer/old_code.py -explainer/conf.py \ No newline at end of file +tutorial/conf.py \ No newline at end of file diff --git a/explainer/SignavioAuthenticator.py b/explainer/SignavioAuthenticator.py deleted file mode 100644 index da86dc3..0000000 --- a/explainer/SignavioAuthenticator.py +++ /dev/null @@ -1,42 +0,0 @@ -import requests - - -class SignavioAuthenticator: - def __init__(self, system_instance, tenant_id, email, pw): - self.system_instance = system_instance - self.tenant_id = tenant_id - self.email = email - self.pw = pw - - """ - Takes care of authentication against Signavio systems - """ - - def authenticate(self): - """ - Authenticates user at Signavio system instance and initiates session. - Returns: - dictionary: Session information - """ - login_url = self.system_instance + "/p/login" - data = {"name": self.email, "password": self.pw, "tokenonly": "true"} - if "tenant_id" in locals(): - data["tenant"] = self.tenant_id - # authenticate - login_request = requests.post(login_url, data) - - # retrieve token and session ID - auth_token = login_request.content.decode("utf-8") - jsesssion_ID = login_request.cookies["JSESSIONID"] - - # The cookie is named 'LBROUTEID' for base_url 'editor.signavio.com' - # and 'editor.signavio.com', and 'AWSELB' for base_url - # 'app-au.signavio.com' and 'app-us.signavio.com' - lb_route_ID = login_request.cookies["LBROUTEID"] - - # return credentials - return { - "jsesssion_ID": jsesssion_ID, - "lb_route_ID": lb_route_ID, - "auth_token": auth_token, - } diff --git a/explainer/explainer_regex.py b/explainer/explainer_regex.py index a7100e1..362d69d 100644 --- a/explainer/explainer_regex.py +++ b/explainer/explainer_regex.py @@ -1,7 +1,7 @@ import math import re from itertools import combinations, product, chain -from explainer_util import ( +from explainer.explainer_util import ( Trace, get_sublists, levenshtein_distance, diff --git a/explainer/explainer_signal.py b/explainer/explainer_signal.py index eff8a85..efb4c97 100644 --- a/explainer/explainer_signal.py +++ b/explainer/explainer_signal.py @@ -1,10 +1,10 @@ -from explainer_util import * +from explainer.explainer_util import * import re import math -import SignavioAuthenticator +from tutorial.SignavioAuthenticator import SignavioAuthenticator from itertools import combinations, chain import requests -from conf import system_instance, workspace_id, user_name, pw +from tutorial.conf import system_instance, workspace_id, user_name, pw KEYWORDS = [ "ABS", @@ -163,7 +163,7 @@ def __init__(self): self.adherent_trace = None self.adherent_traces = [] self.minimal_solution = False - self.authenticator = SignavioAuthenticator.SignavioAuthenticator( + self.authenticator = SignavioAuthenticator( system_instance, workspace_id, user_name, pw ) self.auth_data = self.authenticator.authenticate() diff --git a/explainer/tutorial/diagram.json b/explainer/tutorial/diagram.json deleted file mode 100644 index d3811b0..0000000 --- a/explainer/tutorial/diagram.json +++ /dev/null @@ -1 +0,0 @@ -{"resourceId": "canvas", "formats": {}, "ssextensions": [], "bounds": {"upperLeft": {"x": 0, "y": 0}, "lowerRight": {"x": 1485, "y": 1050}}, "stencilset": {"namespace": "http://b3mn.org/stencilset/bpmn2.0#", "url": "/stencilsets/bpmn2.0/bpmn2.0.json?version=16.16.0"}, "defaultFormats": [{"backgroundColor": "#FFFFFF", "flat": true, "roles": ["Task"]}], "language": "en_us", "stencil": {"id": "BPMNDiagram"}, "properties": {"meta-solutionprocessflows": [], "meta-countryregionrelevance": "", "meta-vertraulichkeit_de_de": "Nur f\u00fcr den internen Gebrauch", "pmcontact": "", "processtype": "None", "auditing": "", "meta-vertraulichkeit_fr_fr": "Nur f\u00fcr den internen Gebrauch", "objective": "", "meta-valuedriver": [], "processowner": "timotheus.kampik@signavio.com", "meta-mcdorganization": "", "processgoal": "", "meta-modularprocess": "", "meta-clouddeliveryprocesschanged": "", "ikskontrolle": [], "meta-efihierarchykey": "", "prozessanalystin": [], "monitoring": "", "version": "", "prozesskategorie": "", "certificationstandards": "", "generalplannedprocessmaturitylev": "", "prozessanalyst": "", "meta-clouddeliveryprocesschanger": [], "dataoutputs": "", "meta-clouddeliveryprocesschangev": [], "meta-usedin": [], "meta-inframework": "", "meta-geltungsbereich": [], "meta-m2detailedprocessdocumentat": [], "isexecutable": "", "expressionlanguage": "http://www.w3.org/1999/XPath", "palevel": "", "prozessinput": "", "exporter": "", "meta-emailadresse": "@vr-finanzdienstleistung.de", "prozessscope": "", "meta-m3annualimprovementtargetsn": "", "meta-m2processcontinuityplan": [], "meta-processparticipant2": "", "meta-processparticipant1": "", "meta-businessunit": "", "meta-verb": "", "meta-maturityassessment": "", "meta-m1knowledgetransfertext": "", "clriskid": "", "m2processvariants": [], "meta-setupguide": "", "topgoalppi2014": "", "meta-bpxtodel": "", "exporterversion": "", "executable": "", "slbespokefla": "", "meta-riskcontrol": "", "meta-scopeitemversion": [], "meta-mcdprocessslasandkpis": "", "meta-vorgangsvorlage": [], "fitfunction": [], "meta-executioncoststotal": "", "meta-processowner": "", "meta-maturityassessmentdetailedv": "", "language": "English", "meta-kundedesoutputs2": [], "meta-clriskid": "N/A", "sflevel": "", "meta-businessdeliverables": "", "meta-clqrqid": "N/A", "meta-m1knowledgetransfertext2": "", "meta-ml": [], "generalactualprocessmaturityleve": "", "meta-m3singletaskingnew": "", "sfsme": "", "slbespokeflag": "No", "meta-clouddeliveryprocesscha": "", "processmanagernew": [], "meta-standardindividual": "", "generalprocessoccurrence": "", "meta-endtoendscenario": "", "externaldocuments": "", "businessunit": "", "signals": "", "meta-m3onlinerealtimeprocessdata": "", "prozessrelevantedokumente": [], "exchange": "", "meta-prozessreifegrad": "", "meta-mcdprocessprimaryresponsibl": "", "meta-tasktutorials": "", "meta-m3processstandardizationnew": "", "prozessoutput": "", "meta-emailadressefg": "@vr-finanzdienstleistung.de", "meta-vertraulichkeit": "Nur f\u00fcr den internen Gebrauch", "isclosed": "", "legalentity": "", "inputsets": "", "m2detailedprocessdocumentation": [], "meta-mynumber": "", "interfaces": "", "m3processvisionlongtermneu": "", "meta-m3continuousimprovementproc": "", "meta-arissapid": "", "outputsets": "", "meta-kpmanager": "", "generalexternalprocessdocumentat": [], "meta-bestpractices": [], "sfmodeller": "", "clqrqid": "", "meta-emailadresseautor": "@vr-finanzdienstleistung.de", "meta-lieferantdesinputs": [], "meta-m2processperformancecockpit": "", "clcontrolid": "", "meta-product": "", "meta-modelingstatus": "Draft", "iso9000ff": "", "typelanguage": "http://www.w3.org/2001/XMLSchema", "processslas": "", "meta-processgoal": "", "meta-mcdlinkofpublishedprocess": "", "businessownernew": [], "processlinks": [], "meta-cycsemanticvalidation": "", "meta-arissaptype": "", "generalprocessstatus": "InProcess", "meta-generalrelateddiagrams": [], "sfregion": "", "meta-generalexternalprocessdocum": [], "effectifprozessverantwortlicherb": "", "messages": "", "meta-deliverymodel": "", "anforderungen": [], "topgoall3process2014": "", "meta-mcdservicearea": "", "datainputs": "", "meta-idwpsrelevant": "", "meta-sapstandardcontent": [], "meta-mcdoperationalprocessid": "", "meta-prozessverantwortlicher2": "", "meta-sfregion": "", "meta-eingangswege": [], "datainputset": "", "flat": "", "meta-mcdprocesschecklist": "", "bpmn2_imports": "", "categories": "", "prozessverantwortlicher": [], "effectifprozessverantwortlicher": "", "meta-vertraulichkeit_es_es": "Nur f\u00fcr den internen Gebrauch", "meta-generalquarterendclosingqec": "", "meta-m2processcontinuitystatus": "", "normen": [], "meta-lastreviewedon": "", "meta-efiideasupport": "", "meta-anweisungenimvorgang": "", "meta-zertifizierung": [], "meta-itsystems": [], "meta-testscript2": "", "meta-setupusingcloudintegrationa": "", "meta-maturitylevel1": "", "meta-lastreviewcommentm": "", "meta-processstatus2": "", "processid": "", "meta-vertraulichkeit_it_it": "Nur f\u00fcr den internen Gebrauch", "meta-itsysteme": [], "meta-prozesseigner": "", "creationdate": "", "meta-m3output": "", "targetnamespace": "http://www.signavio.com/bpmn20", "meta-itapplications": [], "meta-generalprocessnumber": "", "meta-kunde": [], "meta-keyprocessflows": "", "author": "", "market": "", "meta-teilprozesseigner": "", "meta-prozesskategorie": "", "meta-overview": "", "meta-flligam": "", "properties2": "", "meta-legalentity2": "", "meta-accessmaturityassessmenther": "https://workflow.signavio.com/sapprocessmapworkspacesimonblattmann/cases/start/610b89d4017af21cafa24b87", "errors": "", "namespaces": "", "meta-efikeywordsandtags": "", "meta-prozesseinordnung": "", "m2processriskquestionaire": [], "m1processonepagerneu": [], "meta-clcontrolid": "N/A", "bapol3responsiblenew": "", "meta-kennzahlen": [], "meta-vertraulichkeit_nl_nl": "Nur f\u00fcr den internen Gebrauch", "meta-businessbenefits": "", "meta-gltigkeitasdauer": "", "prozessreifegrad": "", "sfopsbu": "", "meta-externaldocuments4": [], "meta-artikelnr": [], "orientation": "horizontal", "sfreviewer": "", "resources": "", "prozessstufe": "", "modificationdate": "", "meta-sh": [], "dataoutputset": "", "itemdefinitions": "", "properties": ""}, "childShapes": [{"outgoing": [{"resourceId": "sid-85147FD1-D065-4B20-9AB9-DC7A04D23C48"}], "resourceId": "sid-38B9A31A-F0F5-4CC6-9215-BEBE7737BC4A", "formats": {}, "dockers": [{"x": 15, "y": 15}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-85147FD1-D065-4B20-9AB9-DC7A04D23C48"}, "bounds": {"upperLeft": {"x": 210.609375, "y": 248}, "lowerRight": {"x": 254.15625, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-7792BFD6-00E6-4610-9FB6-BC7DB3A82EB6"}], "resourceId": "sid-DE01AA8F-B36A-4796-9597-651C8B46C6F3", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-7792BFD6-00E6-4610-9FB6-BC7DB3A82EB6"}, "bounds": {"upperLeft": {"x": 355.2421737945171, "y": 248.26304803033779}, "lowerRight": {"x": 380.1015762054829, "y": 248.39320196966221}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-26C88D1A-96C0-44BD-9BAF-7D25C707388B"}], "resourceId": "sid-25136035-D01E-4DC5-AAF9-FE3F803B865B", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-26C88D1A-96C0-44BD-9BAF-7D25C707388B"}, "bounds": {"upperLeft": {"x": 420.796875, "y": 248.5}, "lowerRight": {"x": 444.1875, "y": 248.5}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-56E77E6D-0667-43B3-855B-AEA6BD2AF93E"}], "resourceId": "sid-088B345E-B2CA-455B-A649-B3E8A3C9157E", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 400.5, "y": 466}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-56E77E6D-0667-43B3-855B-AEA6BD2AF93E"}, "bounds": {"upperLeft": {"x": 400.5, "y": 268.19140625}, "lowerRight": {"x": 464.80078125, "y": 466}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}], "resourceId": "sid-224B5102-FEB8-44B3-9403-387F263E41A5", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 20, "y": 20}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}, "bounds": {"upperLeft": {"x": 663.4140625, "y": 248}, "lowerRight": {"x": 708.1328125, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}], "resourceId": "sid-BE5261E4-44C3-4C0E-A6A9-026989C331D1", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 728.5, "y": 362}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E"}, "bounds": {"upperLeft": {"x": 663.62890625, "y": 268.12109375}, "lowerRight": {"x": 728.5, "y": 362}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": false, "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}], "resourceId": "sid-CA7B85EC-6945-4F46-B7BB-F8208FD568E0", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}, "bounds": {"upperLeft": {"x": 896.6288968799925, "y": 248.2191727137662}, "lowerRight": {"x": 940.6484468700075, "y": 248.4097335362338}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-EA2F6C72-7BD0-4726-84FD-9045BE44B73C"}], "resourceId": "sid-77ADCCB1-8CE0-48B2-BA9C-871C9CBB8214", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-EA2F6C72-7BD0-4726-84FD-9045BE44B73C"}, "bounds": {"upperLeft": {"x": 981.6952985029254, "y": 248.26700968123242}, "lowerRight": {"x": 1005.5351702470746, "y": 248.39314656876758}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-850D153A-D4C2-4853-B59A-13555E19D1A3"}], "resourceId": "sid-B7ED9CDB-A923-458F-AA47-A5E7812477BE", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 14, "y": 14}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-850D153A-D4C2-4853-B59A-13555E19D1A3"}, "bounds": {"upperLeft": {"x": 1106.3671875, "y": 248}, "lowerRight": {"x": 1129.3984375, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}], "resourceId": "sid-DB57838D-7E05-4F45-B36E-765F6F074084", "formats": {}, "dockers": [{"x": 50, "y": 40}, {"x": 961.5, "y": 466}, {"x": 20.5, "y": 20.5}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31"}, "bounds": {"upperLeft": {"x": 565.7080078125, "y": 268.19140625}, "lowerRight": {"x": 961.5, "y": 466}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": "", "bordercolor": "#000000"}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-86ACDE48-0EBD-40FC-B7D0-C4E9D447E4F8"}], "resourceId": "sid-FA8EA2F2-B2E7-46FC-9B60-441B02FDF8F8", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 465.5, "y": 362}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}], "target": {"resourceId": "sid-86ACDE48-0EBD-40FC-B7D0-C4E9D447E4F8"}, "bounds": {"upperLeft": {"x": 465.5, "y": 268.12109375}, "lowerRight": {"x": 562.44921875, "y": 362}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"probability": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "bordercolor": "#000000", "name_de_de": "nein", "transportzeiten": "", "flat": "", "name": "no", "showdiamondmarker": ""}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-DE01AA8F-B36A-4796-9597-651C8B46C6F3"}], "resourceId": "sid-85147FD1-D065-4B20-9AB9-DC7A04D23C48", "formats": {}, "bounds": {"upperLeft": {"x": 255, "y": 208}, "lowerRight": {"x": 355, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Antrag \u00fcberpr\u00fcfen", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Review \nrequest", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-25136035-D01E-4DC5-AAF9-FE3F803B865B"}, {"resourceId": "sid-088B345E-B2CA-455B-A649-B3E8A3C9157E"}], "resourceId": "sid-7792BFD6-00E6-4610-9FB6-BC7DB3A82EB6", "formats": {}, "bounds": {"upperLeft": {"x": 380, "y": 228}, "lowerRight": {"x": 420, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "ParallelGateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "type": "http://b3mn.org/stencilset/bpmn2.0#Exclusive_Databased_Gateway", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "gatewaytype": "AND", "synchronousexecutionjbpm": "", "gate_assignments": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "processid": "", "gates_outgoingsequenceflow": "", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-FA8EA2F2-B2E7-46FC-9B60-441B02FDF8F8"}, {"resourceId": "sid-2C0CB6FB-7B15-4570-975B-89EBCF4072F4"}], "resourceId": "sid-26C88D1A-96C0-44BD-9BAF-7D25C707388B", "formats": {}, "bounds": {"upperLeft": {"x": 445, "y": 228}, "lowerRight": {"x": 485, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Exclusive_Databased_Gateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "adapterconfiguration": "", "gatewaytype": "XOR", "synchronousexecutionjbpm": "", "gate_assignments": "", "question": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "name_de_de": "Standard-\nkonditionen \nanwendbar?", "processid": "", "gates_outgoingsequenceflow": "", "name": "Standard terms\napplicable?", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "x": 68, "y": -24, "valign": "middle", "styles": {"size": "18.0"}, "align": "center"}]}, {"outgoing": [{"resourceId": "sid-224B5102-FEB8-44B3-9403-387F263E41A5"}], "resourceId": "sid-F858CA2F-4719-435E-9FF9-8C906CADF2F9", "formats": {}, "bounds": {"upperLeft": {"x": 563, "y": 208}, "lowerRight": {"x": 663, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Konditionen berechnen", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Calculate \nterms", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-CA7B85EC-6945-4F46-B7BB-F8208FD568E0"}], "resourceId": "sid-49FD1952-AF99-4103-963B-22CB8F6C0BB6", "formats": {}, "bounds": {"upperLeft": {"x": 796, "y": 208}, "lowerRight": {"x": 896, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Vertrag vorbereiten", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Prepare contract", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-77ADCCB1-8CE0-48B2-BA9C-871C9CBB8214"}], "resourceId": "sid-DF31C711-BCD9-4E21-A0F4-07D81A515F31", "formats": {}, "bounds": {"upperLeft": {"x": 941, "y": 228}, "lowerRight": {"x": 981, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "ParallelGateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "type": "http://b3mn.org/stencilset/bpmn2.0#Exclusive_Databased_Gateway", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "gatewaytype": "AND", "synchronousexecutionjbpm": "", "gate_assignments": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "processid": "", "gates_outgoingsequenceflow": "", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-B7ED9CDB-A923-458F-AA47-A5E7812477BE"}], "resourceId": "sid-EA2F6C72-7BD0-4726-84FD-9045BE44B73C", "formats": {}, "bounds": {"upperLeft": {"x": 1006, "y": 208}, "lowerRight": {"x": 1106, "y": 288}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Angebot \nsenden", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Send quote", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [], "resourceId": "sid-850D153A-D4C2-4853-B59A-13555E19D1A3", "formats": {}, "bounds": {"upperLeft": {"x": 1131, "y": 234}, "lowerRight": {"x": 1159, "y": 262}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "EndNoneEvent"}, "dockers": [], "properties": {"datainputs": "", "meta-nachfolgerprozesse": [], "datainput": "", "datainputassociations": "", "auditing": "", "trigger": "None", "monitoring": "", "meta-succeedingprocesses": [], "followingprocesses": [], "bordercolor": "#000000", "datainputassociations_throwevents": "", "folgeprozess": "", "bgcolor": "#ffffff", "name_de_de": "Angebot \ngesendet", "inputset": "", "flat": "", "inputsets": "", "name": "Quote\nsent", "properties2": "", "properties": "", "nachfolgerprozesse": []}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-38B9A31A-F0F5-4CC6-9215-BEBE7737BC4A"}], "resourceId": "sid-1A2C3AFC-4775-461F-9316-ED40DD0B4CA8", "formats": {}, "bounds": {"upperLeft": {"x": 180, "y": 233}, "lowerRight": {"x": 210, "y": 263}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "StartNoneEvent"}, "dockers": [], "properties": {"vorgngerprozesse": [], "meta-preceedingprocesses": [], "preceedingprocesses": [], "auditing": "", "type": "http://b3mn.org/stencilset/bpmn2.0#StartMessageEvent", "frequency": "", "flat": "", "links": [], "meta-vorgngerprozesse": [], "applyincalc": true, "dataoutput": "", "dataoutputassociations": "", "dataoutputassociations_catchevents": "", "trigger": "None", "monitoring": "", "bordercolor": "#000000", "outputset": "", "outputsets": "", "bgcolor": "#ffffff", "name_de_de": "Kredit-Antrag", "processid": "", "name": "Credit\nrequested", "dataoutputs": "", "properties2": "", "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-BE5261E4-44C3-4C0E-A6A9-026989C331D1"}], "resourceId": "sid-86ACDE48-0EBD-40FC-B7D0-C4E9D447E4F8", "formats": {}, "bounds": {"upperLeft": {"x": 563, "y": 322}, "lowerRight": {"x": 663, "y": 402}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Sonder-konditionen vorbereiten", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Prepare special terms", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-DB57838D-7E05-4F45-B36E-765F6F074084"}], "resourceId": "sid-56E77E6D-0667-43B3-855B-AEA6BD2AF93E", "formats": {}, "bounds": {"upperLeft": {"x": 465, "y": 426}, "lowerRight": {"x": 565, "y": 506}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Task"}, "dockers": [], "properties": {"itsysteme": "", "meta-solutionprocessflows": [], "assignments": "", "erteiltfreigabe": "", "completionquantity": 1, "clbcpid": "N/A", "perimetro": "", "outmsgstructure": "", "complexbehaviordefinition": "", "auditing": "", "schwachstellen": "", "mydateishinfo2": "", "glossarlink": "", "meta-erteiltfreigabe2": "", "fhrtzustzlichdurch": "", "meta-myglossarylink": "", "messageref": "", "startid": "", "iso9000ff": "", "additionallyresponsible": "", "applyincalc": true, "meta-processinsightsmetricsavail": "", "adapterconfiguration": "", "isresponsible": "", "loopmaximum": "", "endanchor": "", "meta-istgesamtverwantwortlich": "", "ikskontrolle": [], "testdateianhangantask": "", "testbefore": "", "meta-arissaptype": "", "monitoring": "", "meta-bestpracticedocument": [], "script": "", "outmsgitemkind": "Information", "3": "", "tabmodifiche": "", "vorlagenliste": [], "dataoutputs": "", "descrizioneattivit": "", "meta-erteiltfreigabe": [], "datainputs": "", "looptype": "None", "meta-mcdtaskactivityprerequisite": "", "document3": "", "loopdatainput": "", "completioncondition": "", "document": "", "inmsgimport": "", "document2": "", "instantiate": false, "limitazioni": "", "screenshots": "", "wikiseite": "", "inmsgitemkind": "Information", "datainputset": "", "zahl": "", "onebehavioreventref:": "signal", "innererevision": "", "flat": true, "communicationtype": "", "multilineattribute": "", "categories": "", "liegezeiten": "", "behavior": "all", "meta-signalmetricsavailabletomea": "", "costcenter": "", "risikenundkontrollen": "", "documentout": "", "synchronousexecutionjbpm": "", "callactivitylink": "", "question": "", "decision": "", "meta-risklevel": "None", "operationref": "", "meta-ubmclicks": "", "outputdataitem": "", "inmsgiscollection": "", "meta-itsystems": [], "testtabelle": "", "testliste": [], "meta-wirdinformiert": "", "bordercolor": "#000000", "meta-automatisiert": "", "atoz": "", "meta-riskcontrol": "", "riskstest": "", "meta-dokumentenobjekte": [], "processid": "", "meta-risikenundkontrollen": "", "meta-itsysteme": [], "risikoausprgung": "", "inmsgstructure": "", "booleaninfo": "", "psiconfiguration": "", "arbeitsmittel": "", "meta-momentthatmatters2": "", "meta-wirdkonsultiert": [], "datainputassociations": "", "urlinfo1": [], "urlinfo2": [], "saptalkingnumber": "", "wirdkonsultiert": [], "outmessagename": "", "executiontimefreetext": "", "meta-clriskid": "N/A", "meta-executioncosts": "", "meta-accountable": "", "singlelinetexttest": "", "iks": "", "meta-screenflow": "", "clrqid": "N/A", "calledelement": "", "meta-informed": [], "meta-ml": [], "nonebehavioreventref": "signal", "meta-responsible2": [], "slbespokeflag": "No", "dataoutputassociations": "", "tasktype": "None", "externaldocuments2": [], "loopdataoutput": "", "adapterclassname": "", "outmsgiscollection": "", "meta-processinsightsmetricdetail": [], "externaldocuments": [], "kontrollart": "", "entry": "", "bgcolor": "#FFFFFF", "name_de_de": "Risiken bewerten", "ztoa": "", "loopcondition": "", "dictionaryactivities": "", "scriptformat": "", "prozessrelevantedokumente": [], "name": "Assess risks", "datenbanktabellen": "", "booleaninfo2": "", "meta-riskandcontrols": "", "properties2": "", "functionalrequirements": "", "meta-importance": "", "callacitivity": "", "mydateishinfo": "", "startquantity": 1, "doclink": "", "fhrtdurch": "", "loopcardinality": "", "obiettivo": "", "meta-consulted": [], "cliscontrolflag": "", "inputsets": "", "meta-externaldocuments2": [], "saptransaktionsnummer": "", "meta-externaldocuments4": [], "scopo": "", "inmessagename": "", "wirdinformiert": [], "costs": "", "risikostufe": "", "kontrollmanahmen": "", "descrizionedelleattivit": "", "ikskontrollen": "", "istgesamtverantwortlich": [], "implementation": "webService", "inputdataitem": "", "resources": "", "additionalresponsible": [], "meta-arissapid": "", "externedokumente": "", "meta-sh": [], "outputsets": "", "potentiale": "", "processlink": "", "operationname": "", "dataoutputset": "", "allegatoqualit": [], "anwers": "", "meta-capability": "", "outmsgimport": "", "isforcompensation": "", "bearbeitungszeiten": "", "time": "", "adaptertype": "", "meta-mtmexperiencejourney": [], "properties": ""}, "childShapes": [], "labels": [{"ref": "text_name", "styles": {"size": "18.0"}}]}, {"outgoing": [{"resourceId": "sid-F858CA2F-4719-435E-9FF9-8C906CADF2F9"}], "resourceId": "sid-2C0CB6FB-7B15-4570-975B-89EBCF4072F4", "formats": {}, "dockers": [{"x": 20.5, "y": 20.5}, {"x": 50, "y": 40}], "labels": [{"ref": "text_name", "orientation": "lr", "distance": 6.316500663757324, "x": 523.6798001733436, "y": 248.30278033839545, "from": 0, "valign": "bottom", "styles": {"size": "18.0"}, "to": 1, "align": "right"}], "target": {"resourceId": "sid-F858CA2F-4719-435E-9FF9-8C906CADF2F9"}, "bounds": {"upperLeft": {"x": 484.93749425457406, "y": 248.17135856103246}, "lowerRight": {"x": 562.4492244954259, "y": 248.43411018896754}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"probability": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "bordercolor": "#000000", "name_de_de": "ja", "transportzeiten": "", "flat": "", "name": "yes", "showdiamondmarker": ""}, "childShapes": []}, {"outgoing": [{"resourceId": "sid-04C1509D-573F-4F83-898D-FAD6E7B606DA"}], "resourceId": "sid-04BFD7FE-CDE7-4BAE-A5A1-088ADA1A7A9E", "formats": {}, "bounds": {"upperLeft": {"x": 708, "y": 228}, "lowerRight": {"x": 748, "y": 268}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "Exclusive_Databased_Gateway"}, "dockers": [], "properties": {"assignments": "", "markervisible": "true", "entscheidungstabelle": "", "xortype": "Data", "auditing": "", "gates": "", "flat": "", "lanes": "", "gates_assignments": "", "categories": "", "adapterconfiguration": "", "gatewaytype": "XOR", "synchronousexecutionjbpm": "", "gate_assignments": "", "question": "", "pool": "", "adapterclassname": "", "monitoring": "", "bordercolor": "#000000", "defaultgate": "", "bgcolor": "#ffffff", "processid": "", "gates_outgoingsequenceflow": "", "adaptertype": "", "psiconfiguration": "", "gate_outgoingsequenceflow": ""}, "childShapes": [], "labels": [{"ref": "text_name", "x": 68, "y": -25, "valign": "middle", "styles": {"size": "18.0"}, "align": "center"}]}, {"outgoing": [{"resourceId": "sid-49FD1952-AF99-4103-963B-22CB8F6C0BB6"}], "resourceId": "sid-04C1509D-573F-4F83-898D-FAD6E7B606DA", "formats": {}, "dockers": [{"x": 20, "y": 20}, {"x": 50, "y": 40}], "labels": [], "target": {"resourceId": "sid-49FD1952-AF99-4103-963B-22CB8F6C0BB6"}, "bounds": {"upperLeft": {"x": 748.359375, "y": 248}, "lowerRight": {"x": 795.21875, "y": 248}}, "layers": [], "glossaryLinks": {}, "labelDirtyStates": {}, "stencil": {"id": "SequenceFlow"}, "properties": {"transportzeiten": "", "probability": "", "flat": "", "evaluatestotype": "", "conditiontype": "None", "isimmediate": "", "auditing": "", "conditionexpression": "", "monitoring": "", "expressionlanguage": "", "showdiamondmarker": false, "bordercolor": "#000000"}, "childShapes": []}]} \ No newline at end of file diff --git a/explainer/tutorial/explainer_tutorial_1.ipynb b/tutorial/explainer_tutorial_1.ipynb similarity index 96% rename from explainer/tutorial/explainer_tutorial_1.ipynb rename to tutorial/explainer_tutorial_1.ipynb index 5fee02d..80b7ea5 100644 --- a/explainer/tutorial/explainer_tutorial_1.ipynb +++ b/tutorial/explainer_tutorial_1.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "metadata": { "metadata": {} }, @@ -28,8 +28,8 @@ "source": [ "import sys\n", "sys.path.append('../')\n", - "from explainer_util import Trace, EventLog\n", - "from explainer_regex import ExplainerRegex\n" + "from explainer.explainer_util import Trace, EventLog\n", + "from explainer.explainer_regex import ExplainerRegex\n" ] }, { @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": { "metadata": {} }, @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": { "metadata": {} }, @@ -93,7 +93,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "metadata": { "metadata": {} }, @@ -212,7 +212,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "metadata": { "metadata": {} }, @@ -231,15 +231,18 @@ "--------------------------------\n", "5\n", "\n", - "Addition (Added C at position 5): A->B->A->C->B->C\n", + "Addition (Added B at position 1): A->B->B->A->C->B\n", + "Subtraction (Removed B from position 5): A->B->B->A->C\n", "\n", "Example without minimal solution\n", "--------------------------------\n", "\n", - "Addition (Added C at position 1): C->C->B->A\n", - "Addition (Added A at position 0): A->C->C->B->A\n", - "Addition (Added C at position 4): A->C->C->B->C->A\n", - "Subtraction (Removed A from position 5): A->C->C->B->C\n", + "Addition (Added B at position 1): C->B->B->A\n", + "Addition (Added B at position 1): C->B->B->B->A\n", + "Addition (Added A at position 1): C->A->B->B->B->A\n", + "Subtraction (Removed C from position 0): A->B->B->B->A\n", + "Addition (Added C at position 4): A->B->B->B->C->A\n", + "Subtraction (Removed A from position 5): A->B->B->B->C\n", "\n", "Example with minimal solution\n", "--------------------------------\n", @@ -295,7 +298,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 6, "metadata": { "metadata": {} }, @@ -340,7 +343,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 7, "metadata": { "metadata": {} }, @@ -376,7 +379,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 8, "metadata": { "metadata": {} }, @@ -416,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 9, "metadata": { "metadata": {} }, @@ -451,7 +454,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 10, "metadata": { "metadata": {} }, @@ -477,7 +480,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 11, "metadata": { "metadata": {} }, diff --git a/explainer/tutorial/explainer_tutorial_2.ipynb b/tutorial/explainer_tutorial_2.ipynb similarity index 99% rename from explainer/tutorial/explainer_tutorial_2.ipynb rename to tutorial/explainer_tutorial_2.ipynb index 2d0ef76..94b4e13 100644 --- a/explainer/tutorial/explainer_tutorial_2.ipynb +++ b/tutorial/explainer_tutorial_2.ipynb @@ -21,8 +21,8 @@ "source": [ "import sys\n", "sys.path.append('../')\n", - "from explainer_util import Trace\n", - "from explainer_signal import ExplainerSignal" + "from explainer.explainer_util import Trace\n", + "from explainer.explainer_signal import ExplainerSignal" ] }, { @@ -409,7 +409,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 119/119 [00:00<00:00, 2533615.11it/s]" + "100%|██████████| 119/119 [00:00<00:00, 2918843.13it/s]" ] }, { diff --git a/tutorial/tutorial.ipynb b/tutorial/tutorial.ipynb index 7e7d8a1..046e306 100644 --- a/tutorial/tutorial.ipynb +++ b/tutorial/tutorial.ipynb @@ -488,7 +488,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.10.12" } }, "nbformat": 4,