Spaces:
Sleeping
Sleeping
Demeude Edgar
commited on
Commit
·
d02e800
1
Parent(s):
a0e1005
aba and aba+ working
Browse files- aba/GOAL.txt +0 -34
- aba/aba_framework.py +401 -68
- aba/exemples/{example.txt → exemple.txt} +0 -0
- app.py +17 -0
aba/GOAL.txt
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
Résultat attendu pour le example.txt en aba_plus
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
------- Normal Attacks (by assumption sets) -------
|
| 5 |
-
{c,b,a} attacks {b}
|
| 6 |
-
{c,a} attacks {c,b}
|
| 7 |
-
{c,b,a} attacks {c,a}
|
| 8 |
-
{c,b,a} attacks {c}
|
| 9 |
-
{c,a} attacks {b,a}
|
| 10 |
-
{c,a} attacks {c,a}
|
| 11 |
-
{c,a} attacks {b}
|
| 12 |
-
{c,b,a} attacks {c,b,a}
|
| 13 |
-
{c,b,a} attacks {c,b}
|
| 14 |
-
{c,a} attacks {c,b,a}
|
| 15 |
-
{c,a} attacks {c}
|
| 16 |
-
{c,a} attacks {b,a}
|
| 17 |
-
|
| 18 |
-
------- Reverse Attacks (by assumption sets) -------
|
| 19 |
-
{c,a} attacks {c,b}
|
| 20 |
-
{b,a} attacks {c, b, a}
|
| 21 |
-
{b,a} attacks {c, b}
|
| 22 |
-
{c,b,a} attacks {,b,ac}
|
| 23 |
-
{a} attacks {c,b,c}
|
| 24 |
-
{c,b,a} attacks {c,b}
|
| 25 |
-
{a} attacks {c,b}
|
| 26 |
-
{c,a} attacks {c,b,a}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
---- Resultat atttendu -----
|
| 30 |
-
|
| 31 |
-
Total combinations: 8
|
| 32 |
-
a, ab, ac, abc, c, b, cb, ensemble_vide
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aba/aba_framework.py
CHANGED
|
@@ -17,12 +17,11 @@ from .rule import Rule
|
|
| 17 |
from .attacks import Attacks
|
| 18 |
from collections import deque, defaultdict
|
| 19 |
from itertools import combinations, product
|
| 20 |
-
# from pyvis.network import Network
|
| 21 |
-
|
| 22 |
|
| 23 |
class ABAFramework:
|
| 24 |
"""
|
| 25 |
-
|
|
|
|
| 26 |
|
| 27 |
Attributes:
|
| 28 |
language (set[Literal]): The set of all literals in the framework.
|
|
@@ -36,6 +35,19 @@ class ABAFramework:
|
|
| 36 |
normal_attacks (set[tuple]): Normal ABA+ attacks between assumption sets.
|
| 37 |
reverse_attacks (set[tuple]): Reverse ABA+ attacks due to preferences.
|
| 38 |
assumption_combinations (list[set[Literal]]): All subsets of base assumptions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
"""
|
| 40 |
|
| 41 |
def __init__(self, language: set[Literal], rules: set[Rule], assumptions: set[Literal],
|
|
@@ -62,7 +74,6 @@ class ABAFramework:
|
|
| 62 |
)
|
| 63 |
|
| 64 |
def __str__(self):
|
| 65 |
-
"""Return string representation of ABA framework, including preferences and arguments."""
|
| 66 |
language_str = ', '.join(str(l) for l in sorted(self.language, key=str))
|
| 67 |
rules_str = '\n'.join(str(r) for r in sorted(self.rules, key=str))
|
| 68 |
assumptions_str = ', '.join(str(a) for a in sorted(self.assumptions, key=str))
|
|
@@ -89,14 +100,37 @@ class ABAFramework:
|
|
| 89 |
return hash((frozenset(self.language), frozenset(self.rules),
|
| 90 |
frozenset(self.assumptions), frozenset(self.contraries)))
|
| 91 |
|
| 92 |
-
# ------------------------- Core
|
| 93 |
|
| 94 |
def is_preferred(self, lit1: Literal, lit2: Literal) -> bool:
|
| 95 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
return lit1 in self.preferences and lit2 in self.preferences[lit1]
|
| 97 |
|
| 98 |
def generate_arguments(self) -> None:
|
| 99 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
arg_count = 1
|
| 101 |
arguments_by_claim = defaultdict(set)
|
| 102 |
queue = deque()
|
|
@@ -136,21 +170,298 @@ class ABAFramework:
|
|
| 136 |
self.arguments = set().union(*arguments_by_claim.values())
|
| 137 |
|
| 138 |
def generate_attacks(self) -> None:
|
| 139 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
for arg1 in self.arguments:
|
| 141 |
for arg2 in self.arguments:
|
| 142 |
for contrary in self.contraries:
|
| 143 |
if arg1.claim == contrary.contrary_attacker and contrary.contraried_literal in arg2.leaves:
|
| 144 |
self.attacks.add(Attacks(arg1, arg2))
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
# ------------------------- ABA+ Methods -------------------------
|
| 147 |
|
| 148 |
-
def
|
| 149 |
-
"""
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
source_assumptions = getattr(self, "base_assumptions", self.assumptions)
|
| 155 |
all_combos = []
|
| 156 |
for r in range(len(source_assumptions) + 1):
|
|
@@ -159,21 +470,88 @@ class ABAFramework:
|
|
| 159 |
return all_combos
|
| 160 |
|
| 161 |
def arguments_from_assumptions(self, S: set[Literal]) -> set[Argument]:
|
| 162 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
return {arg for arg in self.arguments if arg.leaves.issubset(S) or (not arg.leaves and set() <= S)}
|
| 164 |
|
| 165 |
def generate_normal_reverse_attacks(self) -> None:
|
| 166 |
"""
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
"""
|
| 173 |
self.normal_attacks.clear()
|
| 174 |
self.reverse_attacks.clear()
|
| 175 |
if not self.assumption_combinations:
|
| 176 |
-
self.assumption_combinations = self.
|
| 177 |
|
| 178 |
for X in self.assumption_combinations:
|
| 179 |
for Y in self.assumption_combinations:
|
|
@@ -218,51 +596,6 @@ class ABAFramework:
|
|
| 218 |
self.normal_attacks.update(additional_normal)
|
| 219 |
self.reverse_attacks.update(additional_reverse)
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
if not getattr(self, "base_assumptions", None):
|
| 225 |
-
self.base_assumptions = set(self.assumptions)
|
| 226 |
-
self.assumption_combinations = self._generate_assumption_combinations()
|
| 227 |
-
print(f"Generated {len(self.assumption_combinations)} assumption combinations")
|
| 228 |
-
self.generate_normal_reverse_attacks()
|
| 229 |
-
print(f"Generated {len(self.normal_attacks)} normal attacks (assumption sets)")
|
| 230 |
-
print(f"Generated {len(self.reverse_attacks)} reverse attacks (assumption sets)")
|
| 231 |
-
|
| 232 |
-
# # ------------------------- Visualization -------------------------
|
| 233 |
-
|
| 234 |
-
# def plot_aba_plus_graph(self, output_html="aba_plus_graph.html"):
|
| 235 |
-
# """
|
| 236 |
-
# Visualize ABA+ framework with PyVis.
|
| 237 |
-
# - Normal attacks: solid black lines
|
| 238 |
-
# - Reverse attacks: dashed red lines
|
| 239 |
-
# Nodes: labeled with assumption sets
|
| 240 |
-
# """
|
| 241 |
-
# if not hasattr(self, "normal_attacks") or not hasattr(self, "reverse_attacks"):
|
| 242 |
-
# print("Warning: ABA+ attacks not generated. Call make_aba_plus() first.")
|
| 243 |
-
# return
|
| 244 |
-
|
| 245 |
-
# net = Network(directed=True, notebook=False, height="750px", width="100%")
|
| 246 |
-
# net.set_options("""
|
| 247 |
-
# {
|
| 248 |
-
# "physics": {"enabled": true, "stabilization": {"enabled": true, "iterations": 200}}
|
| 249 |
-
# }
|
| 250 |
-
# """)
|
| 251 |
-
|
| 252 |
-
# # Add nodes labeled by assumption sets
|
| 253 |
-
# for S in self.assumption_combinations:
|
| 254 |
-
# label = "{" + ",".join(str(l) for l in sorted(S, key=str)) + "}" if S else "{}"
|
| 255 |
-
# net.add_node(str(frozenset(S)), label=label)
|
| 256 |
-
|
| 257 |
-
# # Normal attacks
|
| 258 |
-
# for att in self.normal_attacks:
|
| 259 |
-
# net.add_edge(str(att[0]), str(att[1]), color='black', dashes=False, width=2, title="Normal Attack")
|
| 260 |
-
|
| 261 |
-
# # Reverse attacks
|
| 262 |
-
# for att in self.reverse_attacks:
|
| 263 |
-
# net.add_edge(str(att[0]), str(att[1]), color='red', dashes=True, width=2, title="Reverse Attack")
|
| 264 |
-
|
| 265 |
-
# net.write_html(output_html)
|
| 266 |
-
# print(f"ABA+ attack graph saved to {output_html}")
|
| 267 |
-
# print(f" - Normal attacks: {len(self.normal_attacks)}")
|
| 268 |
-
# print(f" - Reverse attacks: {len(self.reverse_attacks)}")
|
|
|
|
| 17 |
from .attacks import Attacks
|
| 18 |
from collections import deque, defaultdict
|
| 19 |
from itertools import combinations, product
|
|
|
|
|
|
|
| 20 |
|
| 21 |
class ABAFramework:
|
| 22 |
"""
|
| 23 |
+
Formal specification and implementation of Assumption-Based Argumentation (ABA) frameworks
|
| 24 |
+
with support for preference-based extensions (ABA+).
|
| 25 |
|
| 26 |
Attributes:
|
| 27 |
language (set[Literal]): The set of all literals in the framework.
|
|
|
|
| 35 |
normal_attacks (set[tuple]): Normal ABA+ attacks between assumption sets.
|
| 36 |
reverse_attacks (set[tuple]): Reverse ABA+ attacks due to preferences.
|
| 37 |
assumption_combinations (list[set[Literal]]): All subsets of base assumptions.
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
Mathematical foundation :
|
| 41 |
+
|
| 42 |
+
An ABA framework is a tuple ⟨L, R, A, C⟩ where:
|
| 43 |
+
L ⊆ Lit: The language; a finite set of literals
|
| 44 |
+
R ⊆ Rules: The set of rules of the form h ← b₁, ..., bₙ where h ∈ L and bᵢ ∈ L
|
| 45 |
+
A ⊆ L: The set of assumptions; A and (Lit \ L) partition the language
|
| 46 |
+
C ⊆ A × L: The contrary relation; for each assumption a ∈ A, C(a) is its contrary
|
| 47 |
+
|
| 48 |
+
An ABA+ framework extends ABA by adding:
|
| 49 |
+
P ⊆ A × A: A preference relation over assumptions (typically antisymmetric)
|
| 50 |
+
|
| 51 |
"""
|
| 52 |
|
| 53 |
def __init__(self, language: set[Literal], rules: set[Rule], assumptions: set[Literal],
|
|
|
|
| 74 |
)
|
| 75 |
|
| 76 |
def __str__(self):
|
|
|
|
| 77 |
language_str = ', '.join(str(l) for l in sorted(self.language, key=str))
|
| 78 |
rules_str = '\n'.join(str(r) for r in sorted(self.rules, key=str))
|
| 79 |
assumptions_str = ', '.join(str(a) for a in sorted(self.assumptions, key=str))
|
|
|
|
| 100 |
return hash((frozenset(self.language), frozenset(self.rules),
|
| 101 |
frozenset(self.assumptions), frozenset(self.contraries)))
|
| 102 |
|
| 103 |
+
# ------------------------- Core Methods -------------------------
|
| 104 |
|
| 105 |
def is_preferred(self, lit1: Literal, lit2: Literal) -> bool:
|
| 106 |
+
"""
|
| 107 |
+
Determines whether the first literal is strictly preferred over the second one.
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
bool: True if lit1 is strictly preferred over lit2 according to the framework's
|
| 111 |
+
preference relation (i.e., lit1 > lit2). False otherwise.
|
| 112 |
+
"""
|
| 113 |
return lit1 in self.preferences and lit2 in self.preferences[lit1]
|
| 114 |
|
| 115 |
def generate_arguments(self) -> None:
|
| 116 |
+
"""
|
| 117 |
+
Generates all possible arguments in the ABA framework based on the rules, assumptions, and contraries.
|
| 118 |
+
|
| 119 |
+
This method populates the self.arguments set with all arguments that can be constructed from the framework.
|
| 120 |
+
|
| 121 |
+
Example:
|
| 122 |
+
Given:
|
| 123 |
+
L = {a, b, x, y}
|
| 124 |
+
A = {a, b}
|
| 125 |
+
R = { r1: x ← a, b r2: y ← x r3: z ← }
|
| 126 |
+
|
| 127 |
+
Arguments generated:
|
| 128 |
+
A1: ⟨{a} ↦ a⟩
|
| 129 |
+
A2: ⟨{b} ↦ b⟩
|
| 130 |
+
A3: ⟨∅ ↦ z⟩
|
| 131 |
+
A4: ⟨{a, b} ↦ x⟩ (via r1, using A1 and A2)
|
| 132 |
+
A5: ⟨{a, b} ↦ y⟩ (via r2, using A4)
|
| 133 |
+
"""
|
| 134 |
arg_count = 1
|
| 135 |
arguments_by_claim = defaultdict(set)
|
| 136 |
queue = deque()
|
|
|
|
| 170 |
self.arguments = set().union(*arguments_by_claim.values())
|
| 171 |
|
| 172 |
def generate_attacks(self) -> None:
|
| 173 |
+
"""
|
| 174 |
+
Generates all possible attacks between arguments based on the contraries in the ABA framework.
|
| 175 |
+
|
| 176 |
+
This method populates the self.attacks set with all attacks that can be constructed from the framework.
|
| 177 |
+
"""
|
| 178 |
for arg1 in self.arguments:
|
| 179 |
for arg2 in self.arguments:
|
| 180 |
for contrary in self.contraries:
|
| 181 |
if arg1.claim == contrary.contrary_attacker and contrary.contraried_literal in arg2.leaves:
|
| 182 |
self.attacks.add(Attacks(arg1, arg2))
|
| 183 |
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# ------------------------- ABA Methods -------------------------
|
| 187 |
+
|
| 188 |
+
def transform_aba(self) -> None:
|
| 189 |
+
"""
|
| 190 |
+
Transforms the ABA framework to ensure it is both non-circular and atomic.
|
| 191 |
+
|
| 192 |
+
Procedure:
|
| 193 |
+
1. Checks if the framework is circular using is_aba_circular().
|
| 194 |
+
2. If it is circular, calls _make_aba_not_circular() to remove circularity.
|
| 195 |
+
3. If it is not circular but not atomic, calls _make_aba_atomic() to ensure atomicity.
|
| 196 |
+
4. The transformation is performed in-place and modifies the framework's rules and language as needed.
|
| 197 |
+
|
| 198 |
+
After calling this function, the ABA framework will be non-circular and atomic.
|
| 199 |
+
"""
|
| 200 |
+
print("\n ------- Transforming ABA framework -------\n")
|
| 201 |
+
if self.is_aba_circular():
|
| 202 |
+
print("The ABA Framework is circular\n")
|
| 203 |
+
self.make_aba_not_circular()
|
| 204 |
+
elif not self.is_aba_atomic():
|
| 205 |
+
print("The ABA Framework is not atomic\n")
|
| 206 |
+
self.make_aba_atomic()
|
| 207 |
+
|
| 208 |
+
def make_aba_atomic(self) -> None:
|
| 209 |
+
"""
|
| 210 |
+
Transforms the ABA framework into an atomic one.
|
| 211 |
+
|
| 212 |
+
Procedure:
|
| 213 |
+
1. For each literal x in the language that is not an assumption:
|
| 214 |
+
- Introduce two new literals: xd and xnd (both are assumptions).
|
| 215 |
+
- Add both to the language and assumptions.
|
| 216 |
+
2. For each rule:
|
| 217 |
+
- Replace non-assumption literals in the body with their 'xd' counterparts.
|
| 218 |
+
3. For each new pair (xd, xnd):
|
| 219 |
+
- Add contraries: Contrary(xd, xnd) and Contrary(xnd, x).
|
| 220 |
+
|
| 221 |
+
After this transformation, all rule bodies contain only assumptions.
|
| 222 |
+
|
| 223 |
+
Example:
|
| 224 |
+
Original framework:
|
| 225 |
+
L = {a, b, x}
|
| 226 |
+
A = {a, b}
|
| 227 |
+
R = { r1: a <- x }
|
| 228 |
+
|
| 229 |
+
After _make_aba_atomic():
|
| 230 |
+
L = {a, b, x, xd, xnd}
|
| 231 |
+
A = {a, b, xd, xnd}
|
| 232 |
+
R = { a <- xd }
|
| 233 |
+
Contraries = { xd̄ = xnd, xnd̄ = x }
|
| 234 |
+
"""
|
| 235 |
+
new_language = set(self.language)
|
| 236 |
+
new_assumptions = set(self.assumptions)
|
| 237 |
+
new_rules = set()
|
| 238 |
+
new_contraries = set(self.contraries)
|
| 239 |
+
|
| 240 |
+
# Step 1: Create xd and xnd for each non-assumption literal
|
| 241 |
+
mapping = {} # maps original non-assumption literal -> xd
|
| 242 |
+
for lit in self.language:
|
| 243 |
+
if lit not in self.assumptions:
|
| 244 |
+
xd = Literal(f"{lit}d")
|
| 245 |
+
xnd = Literal(f"{lit}nd")
|
| 246 |
+
new_language.update({xd, xnd})
|
| 247 |
+
new_assumptions.update({xd, xnd})
|
| 248 |
+
mapping[lit] = xd
|
| 249 |
+
# Add contraries
|
| 250 |
+
new_contraries.add(Contrary(xd, xnd))
|
| 251 |
+
new_contraries.add(Contrary(xnd, lit))
|
| 252 |
+
|
| 253 |
+
# Step 2: Replace non-assumptions in rule bodies with xd
|
| 254 |
+
for rule in self.rules:
|
| 255 |
+
new_body = set()
|
| 256 |
+
for lit in rule.body:
|
| 257 |
+
if lit in mapping: # replace non-assumption with xd
|
| 258 |
+
new_body.add(mapping[lit])
|
| 259 |
+
else:
|
| 260 |
+
new_body.add(lit)
|
| 261 |
+
new_rules.add(Rule(rule.rule_name, rule.head, new_body))
|
| 262 |
+
|
| 263 |
+
# Step 3: Update framework
|
| 264 |
+
self.language = new_language
|
| 265 |
+
self.assumptions = new_assumptions
|
| 266 |
+
self.rules = new_rules
|
| 267 |
+
self.contraries = new_contraries
|
| 268 |
+
|
| 269 |
+
def is_aba_atomic(self) -> bool:
|
| 270 |
+
"""
|
| 271 |
+
Checks if the ABA framework is atomic.
|
| 272 |
+
|
| 273 |
+
An ABA framework is atomic if every rule's body (if non-empty) consists only of assumptions.
|
| 274 |
+
Returns:
|
| 275 |
+
bool: True if the framework is atomic, False otherwise.
|
| 276 |
+
"""
|
| 277 |
+
for rule in self.rules:
|
| 278 |
+
if rule.body and not all(lit in self.assumptions for lit in rule.body):
|
| 279 |
+
return False
|
| 280 |
+
return True
|
| 281 |
+
|
| 282 |
+
def is_aba_circular(self) -> bool:
|
| 283 |
+
"""
|
| 284 |
+
Checks if the ABA framework is circular by detecting cycles in the rule dependency graph.
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
bool: True if the framework is circular (i.e., contains a cycle), False otherwise.
|
| 288 |
+
|
| 289 |
+
Procedure:
|
| 290 |
+
- The dependency graph is constructed where each node is a literal.
|
| 291 |
+
- For each rule, an edge is added from every literal in the rule's body to the rule's head.
|
| 292 |
+
- A cycle in this graph means there is a sequence of rules such that a literal can be derived from itself,
|
| 293 |
+
directly or indirectly, which is the definition of circularity in ABA frameworks.
|
| 294 |
+
- The function uses depth-first search (DFS) to detect cycles in the graph.
|
| 295 |
+
"""
|
| 296 |
+
# Build adjacency list: for each literal, store the set of literals it can reach via rules
|
| 297 |
+
adj = {lit: set() for lit in self.language}
|
| 298 |
+
for rule in self.rules:
|
| 299 |
+
for body_lit in rule.body:
|
| 300 |
+
adj[body_lit].add(rule.head)
|
| 301 |
+
|
| 302 |
+
def has_cycle(lit, visited, stack):
|
| 303 |
+
"""
|
| 304 |
+
Helper function to perform DFS and detect cycles.
|
| 305 |
+
|
| 306 |
+
Args:
|
| 307 |
+
lit: The current literal being visited.
|
| 308 |
+
visited: Set of literals that have been fully explored.
|
| 309 |
+
stack: Set of literals in the current DFS path (recursion stack).
|
| 310 |
+
|
| 311 |
+
Returns:
|
| 312 |
+
True if a cycle is detected starting from 'lit', False otherwise.
|
| 313 |
+
"""
|
| 314 |
+
visited.add(lit)
|
| 315 |
+
stack.add(lit)
|
| 316 |
+
for neighbor in adj.get(lit, []):
|
| 317 |
+
if neighbor not in visited:
|
| 318 |
+
if has_cycle(neighbor, visited, stack):
|
| 319 |
+
return True
|
| 320 |
+
elif neighbor in stack:
|
| 321 |
+
# Found a back edge, which means a cycle exists
|
| 322 |
+
return True
|
| 323 |
+
stack.remove(lit)
|
| 324 |
+
return False
|
| 325 |
+
|
| 326 |
+
visited = set()
|
| 327 |
+
# Check for cycles starting from each literal in the language
|
| 328 |
+
for lit in self.language:
|
| 329 |
+
if lit not in visited:
|
| 330 |
+
if has_cycle(lit, visited, set()):
|
| 331 |
+
return True # Cycle found
|
| 332 |
+
return False # No cycles
|
| 333 |
+
|
| 334 |
+
def make_aba_not_circular(self) -> None:
|
| 335 |
+
"""
|
| 336 |
+
Transforms the ABA framework to a non-circular one by renaming heads and bodies of rules.
|
| 337 |
+
|
| 338 |
+
Procedure:
|
| 339 |
+
1. Compute k = |language| - |assumptions|.
|
| 340 |
+
2. For each atomic rule (body is empty or only contains assumptions):
|
| 341 |
+
- Create new rules with heads renamed as x1, x2, ..., x(k-1) with the same body.
|
| 342 |
+
- Keep the original atomic rule.
|
| 343 |
+
3. For each non-atomic rule (body contains non-assumptions):
|
| 344 |
+
- Create k-1 new rules with heads renamed as x2, x3, ... and bodies renamed by iteration index.
|
| 345 |
+
- In the last iteration (i = k-1), keep the original head but update the body with the last renamed literals.
|
| 346 |
+
4. Update the framework's language and rules with the new transformed rules.
|
| 347 |
+
|
| 348 |
+
After this transformation, circular dependencies in arguments are eliminated.
|
| 349 |
+
The function modifies the ABAFramework in-place and does not return any value.
|
| 350 |
+
|
| 351 |
+
Example:
|
| 352 |
+
Original framework:
|
| 353 |
+
Language: {a, b, x, y}
|
| 354 |
+
Assumptions: {a, b}
|
| 355 |
+
Rules:
|
| 356 |
+
r1: y <- y
|
| 357 |
+
r2: x <- x
|
| 358 |
+
r3: x <- a
|
| 359 |
+
|
| 360 |
+
After _make_aba_not_circular():
|
| 361 |
+
New rules:
|
| 362 |
+
y <- y1
|
| 363 |
+
x <- x1
|
| 364 |
+
x1 <- a
|
| 365 |
+
x <- a
|
| 366 |
+
"""
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
k = len(self.language) - len(self.assumptions)
|
| 370 |
+
new_language = set(self.language)
|
| 371 |
+
new_rules = set()
|
| 372 |
+
|
| 373 |
+
for rule in self.rules:
|
| 374 |
+
if not rule.body or all(lit in self.assumptions for lit in rule.body):
|
| 375 |
+
# Atomic rule: create x1, x2, ..., x(k-1)
|
| 376 |
+
for i in range(1, k):
|
| 377 |
+
new_head = Literal(f"{rule.head}{i}")
|
| 378 |
+
new_body = set(rule.body) # <<< fix: define new_body here
|
| 379 |
+
new_language.add(new_head)
|
| 380 |
+
new_rule_name = f"{rule.rule_name}_{i+1}" # unique name
|
| 381 |
+
new_rules.add(Rule(new_rule_name, new_head, new_body))
|
| 382 |
+
new_rules.add(rule) # Keep original
|
| 383 |
+
|
| 384 |
+
else:
|
| 385 |
+
# Non-atomic: rename head and body
|
| 386 |
+
for i in range(1, k):
|
| 387 |
+
if i < k - 1:
|
| 388 |
+
new_head = Literal(f"{rule.head}{i+1}")
|
| 389 |
+
else:
|
| 390 |
+
new_head = rule.head # last iteration keeps original head
|
| 391 |
+
new_body = {Literal(f"{lit}{i}") for lit in rule.body}
|
| 392 |
+
new_rule_name = f"{rule.rule_name}_{i+1}"
|
| 393 |
+
new_rules.add(Rule(new_rule_name, new_head, new_body))
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
self.language = new_language
|
| 397 |
+
self.rules = new_rules
|
| 398 |
+
|
| 399 |
+
|
| 400 |
# ------------------------- ABA+ Methods -------------------------
|
| 401 |
|
| 402 |
+
def make_aba_plus(self) -> None:
|
| 403 |
+
"""
|
| 404 |
+
Generates the ABA+ framework by creating all assumption set combinations and computing attacks.
|
| 405 |
+
|
| 406 |
+
ABA+ extends classical ABA by incorporating preference information over assumptions directly into
|
| 407 |
+
the attack relation, allowing for attack reversal when a target assumption is preferred over an
|
| 408 |
+
attacking assumption.
|
| 409 |
+
|
| 410 |
+
Procedure:
|
| 411 |
+
1. Ensures base_assumptions is set (preserves original assumptions before any transformation).
|
| 412 |
+
2. Generates all possible subsets of base assumptions (the power set).
|
| 413 |
+
3. Computes both normal attacks and reverse attacks between assumption sets based on:
|
| 414 |
+
- Classical ABA attack rules (arguments attacking assumptions)
|
| 415 |
+
- Preference-based attack reversal (when target assumptions are preferred)
|
| 416 |
+
|
| 417 |
+
After calling this function:
|
| 418 |
+
- self.assumption_combinations contains all subsets of base assumptions
|
| 419 |
+
- self.normal_attacks contains standard attacks between assumption sets
|
| 420 |
+
- self.reverse_attacks contains preference-reversed attacks between assumption sets
|
| 421 |
+
|
| 422 |
+
Note:
|
| 423 |
+
This method should be called after generate_arguments() has been executed, as it relies
|
| 424 |
+
on the generated arguments to determine attacks between assumption sets.
|
| 425 |
+
"""
|
| 426 |
+
|
| 427 |
|
| 428 |
+
if not getattr(self, "base_assumptions", None):
|
| 429 |
+
self.base_assumptions = set(self.assumptions)
|
| 430 |
+
self.assumption_combinations = self.generate_assumption_combinations()
|
| 431 |
+
print(f"Generated {len(self.assumption_combinations)} assumption combinations")
|
| 432 |
+
|
| 433 |
+
self.generate_normal_reverse_attacks()
|
| 434 |
+
print(f"Generated {len(self.normal_attacks)} normal attacks (assumption sets)")
|
| 435 |
+
print(f"Generated {len(self.reverse_attacks)} reverse attacks (assumption sets)")
|
| 436 |
+
|
| 437 |
+
def generate_assumption_combinations(self) -> list[set[Literal]]:
|
| 438 |
+
"""
|
| 439 |
+
Generates all possible subsets of base assumptions (the power set).
|
| 440 |
+
|
| 441 |
+
This method creates every possible combination of assumptions, from the empty set to the complete
|
| 442 |
+
set of all base assumptions. Each combination represents a potential stance or position in the
|
| 443 |
+
argumentation framework.
|
| 444 |
+
|
| 445 |
+
Returns:
|
| 446 |
+
list[set[Literal]]: A list where each element is a set of literals representing one possible
|
| 447 |
+
combination of assumptions. The list includes:
|
| 448 |
+
- The empty set ∅
|
| 449 |
+
- All singleton sets {a} for each assumption a
|
| 450 |
+
- All pairs, triples, etc., up to the full set of assumptions
|
| 451 |
+
|
| 452 |
+
Example:
|
| 453 |
+
If base_assumptions = {a, b}, the result will be:
|
| 454 |
+
[
|
| 455 |
+
set(), # empty set
|
| 456 |
+
{a}, # singleton
|
| 457 |
+
{b}, # singleton
|
| 458 |
+
{a, b} # complete set
|
| 459 |
+
]
|
| 460 |
+
|
| 461 |
+
Note:
|
| 462 |
+
For n base assumptions, this generates 2^n combinations. The computational complexity
|
| 463 |
+
can become significant for large assumption sets.
|
| 464 |
+
"""
|
| 465 |
source_assumptions = getattr(self, "base_assumptions", self.assumptions)
|
| 466 |
all_combos = []
|
| 467 |
for r in range(len(source_assumptions) + 1):
|
|
|
|
| 470 |
return all_combos
|
| 471 |
|
| 472 |
def arguments_from_assumptions(self, S: set[Literal]) -> set[Argument]:
|
| 473 |
+
"""
|
| 474 |
+
Returns all arguments that can be constructed using only assumptions from set S.
|
| 475 |
+
|
| 476 |
+
An argument is valid for assumption set S if all its supporting assumptions (leaves) are
|
| 477 |
+
contained within S. This is crucial for determining which arguments are available when
|
| 478 |
+
reasoning from a particular set of assumptions.
|
| 479 |
+
|
| 480 |
+
Args:
|
| 481 |
+
S (set[Literal]): A set of assumptions to check against.
|
| 482 |
+
|
| 483 |
+
Returns:
|
| 484 |
+
set[Argument]: The set of all arguments whose leaves (supporting assumptions) are
|
| 485 |
+
subsets of S. This includes:
|
| 486 |
+
- Arguments with leaves ⊆ S
|
| 487 |
+
- Arguments with no leaves (derived from rules with empty bodies)
|
| 488 |
+
|
| 489 |
+
Example:
|
| 490 |
+
Given:
|
| 491 |
+
- Argument A1 with leaves {a, b}
|
| 492 |
+
- Argument A2 with leaves {a}
|
| 493 |
+
- Argument A3 with leaves {c}
|
| 494 |
+
- S = {a, b}
|
| 495 |
+
|
| 496 |
+
Returns: {A1, A2} (A3 is excluded because c ∉ S)
|
| 497 |
+
"""
|
| 498 |
return {arg for arg in self.arguments if arg.leaves.issubset(S) or (not arg.leaves and set() <= S)}
|
| 499 |
|
| 500 |
def generate_normal_reverse_attacks(self) -> None:
|
| 501 |
"""
|
| 502 |
+
Generates both normal and reverse attacks between assumption sets in ABA+.
|
| 503 |
+
|
| 504 |
+
This is the core method for implementing preference-based attack reversal in ABA+. It examines
|
| 505 |
+
all pairs of assumption sets and determines whether attacks exist and whether they should be
|
| 506 |
+
reversed based on preference information.
|
| 507 |
+
|
| 508 |
+
Attack Types:
|
| 509 |
+
1. Normal Attack (X attacks Y):
|
| 510 |
+
- There exists an argument from X that attacks an argument from Y
|
| 511 |
+
- The attack is based on contraries (classical ABA attack)
|
| 512 |
+
- No preference reversal occurs (i.e., no assumption in Y is preferred over assumptions in X)
|
| 513 |
+
|
| 514 |
+
2. Reverse Attack (Y attacks X):
|
| 515 |
+
- There would normally be an attack from X to Y
|
| 516 |
+
- BUT some assumption y ∈ Y is strictly preferred over some assumption x ∈ X
|
| 517 |
+
- The preference relationship reverses the direction of the attack
|
| 518 |
+
|
| 519 |
+
Procedure:
|
| 520 |
+
1. Clears any existing normal and reverse attacks.
|
| 521 |
+
2. Generates assumption combinations if not already done.
|
| 522 |
+
3. For each pair of assumption sets (X, Y):
|
| 523 |
+
a. Gets all arguments constructible from X and Y respectively.
|
| 524 |
+
b. Checks if any argument from X attacks any argument from Y (via contraries).
|
| 525 |
+
c. If an attack exists:
|
| 526 |
+
- Checks if any assumption in Y is preferred over any assumption in X.
|
| 527 |
+
- If yes → creates a reverse attack (Y attacks X).
|
| 528 |
+
- If no → creates a normal attack (X attacks Y).
|
| 529 |
+
4. Adds subset closure attacks:
|
| 530 |
+
- If X attacks Y normally, then any superset of X also attacks Y.
|
| 531 |
+
- If Y attacks X in reverse, then any superset of Y also attacks X.
|
| 532 |
+
|
| 533 |
+
Mathematical Definition:
|
| 534 |
+
- Normal: X ↝ Y iff ∃ax ∈ Args(X), ∃ay ∈ Args(Y): ax attacks ay ∧ ¬(∃y∈Y, ∃x∈X: y > x)
|
| 535 |
+
- Reverse: Y ↝ X iff ∃ax ∈ Args(X), ∃ay ∈ Args(Y): ax attacks ay ∧ (∃y∈Y, ∃x∈X: y > x)
|
| 536 |
+
|
| 537 |
+
Side Effects:
|
| 538 |
+
Populates self.normal_attacks and self.reverse_attacks with frozenset tuples representing
|
| 539 |
+
the attack relationships between assumption sets.
|
| 540 |
+
|
| 541 |
+
Example:
|
| 542 |
+
Given:
|
| 543 |
+
- Assumptions: {a, b, c}
|
| 544 |
+
- Preferences: b > a (b is preferred over a)
|
| 545 |
+
- Argument from {a} attacks argument from {b}
|
| 546 |
+
|
| 547 |
+
Result:
|
| 548 |
+
- Without preferences: {a} ↝ {b} (normal attack)
|
| 549 |
+
- With preferences: {b} ↝ {a} (reverse attack, because b > a)
|
| 550 |
"""
|
| 551 |
self.normal_attacks.clear()
|
| 552 |
self.reverse_attacks.clear()
|
| 553 |
if not self.assumption_combinations:
|
| 554 |
+
self.assumption_combinations = self.generate_assumption_combinations()
|
| 555 |
|
| 556 |
for X in self.assumption_combinations:
|
| 557 |
for Y in self.assumption_combinations:
|
|
|
|
| 596 |
self.normal_attacks.update(additional_normal)
|
| 597 |
self.reverse_attacks.update(additional_reverse)
|
| 598 |
|
| 599 |
+
def arguments_from_assumptions(self, S: set[Literal]) -> set[Argument]:
|
| 600 |
+
"""Return arguments whose leaves are subsets of S."""
|
| 601 |
+
return {arg for arg in self.arguments if arg.leaves.issubset(S) or (not arg.leaves and set() <= S)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aba/exemples/{example.txt → exemple.txt}
RENAMED
|
File without changes
|
app.py
CHANGED
|
@@ -106,6 +106,23 @@ async def aba_upload(file: UploadFile = File(...)):
|
|
| 106 |
content = await file.read()
|
| 107 |
text = content.decode("utf-8")
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
aba_framework = build_aba_framework_from_text(text)
|
| 110 |
aba_framework = prepare_aba_plus_framework(aba_framework)
|
| 111 |
aba_framework.make_aba_plus()
|
|
|
|
| 106 |
content = await file.read()
|
| 107 |
text = content.decode("utf-8")
|
| 108 |
|
| 109 |
+
aba_framework = build_aba_framework_from_text(text)
|
| 110 |
+
aba_framework.generate_arguments()
|
| 111 |
+
aba_framework.generate_attacks()
|
| 112 |
+
|
| 113 |
+
results = {
|
| 114 |
+
"assumptions": [str(a) for a in aba_framework.assumptions],
|
| 115 |
+
"arguments": [str(arg) for arg in aba_framework.arguments],
|
| 116 |
+
"attacks": [str(att) for att in aba_framework.attacks],
|
| 117 |
+
}
|
| 118 |
+
return results
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@app.post("/aba-plus-upload")
|
| 122 |
+
async def aba_upload(file: UploadFile = File(...)):
|
| 123 |
+
content = await file.read()
|
| 124 |
+
text = content.decode("utf-8")
|
| 125 |
+
|
| 126 |
aba_framework = build_aba_framework_from_text(text)
|
| 127 |
aba_framework = prepare_aba_plus_framework(aba_framework)
|
| 128 |
aba_framework.make_aba_plus()
|