Skip to content
6 changes: 4 additions & 2 deletions src/argumentation/ranking/ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,12 @@ def discussion_based_ranking(
converged = True
for argument in framework.arguments:
frontier = {argument: 1}
sequence: list[float] = []
# Exact integer counts: converting to float overflows on cycles and
# loses the precision the lexicographic comparison relies on.
sequence: list[int] = []
for length in range(1, depth + 1):
count = sum(frontier.values())
sequence.append(float(-count if length % 2 == 1 else count))
sequence.append(-count if length % 2 == 1 else count)
next_frontier: dict[str, int] = {}
for target, multiplicity in frontier.items():
for attacker in attackers[target]:
Expand Down
57 changes: 22 additions & 35 deletions src/argumentation/ranking/ranking_axioms.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ def self_contradiction(
framework: ArgumentationFramework,
result: RankingResult,
) -> bool:
"""Check Bonzon et al. 2016 p. 1 self-contradiction precedence."""
"""Check Bonzon et al. 2016 p. 2 self-contradiction (SC).

Every non-self-attacking argument ranks strictly above every
self-attacking one: (a, a) not in R and (b, b) in R imply a > b.
"""

self_attacking = {
argument
Expand All @@ -139,7 +143,7 @@ def self_contradiction(
}
clean = set(framework.arguments) - self_attacking
return all(
not result.strictly_prefers(self_attacker, other)
result.strictly_prefers(other, self_attacker)
for self_attacker in self_attacking
for other in clean
)
Expand All @@ -149,20 +153,22 @@ def defense_precedence(
framework: ArgumentationFramework,
result: RankingResult,
) -> bool:
"""Check Amgoud and Ben-Naim 2013 p. 5 defense precedence."""
"""Check Amgoud and Ben-Naim 2013 p. 5 defense precedence (DP).

Bonzon et al. 2016 p. 2: |R1-(a)| = |R1-(b)|, R2+(a) nonempty and
R2+(b) empty imply a > b. A single defender suffices; not every attacker
of ``a`` needs to be attacked (Amgoud and Ben-Naim 2013, Example 3).
"""

attackers = _attackers(framework)
for defended in framework.arguments:
defended_attackers = attackers[defended]
if not defended_attackers or not _every_attacker_is_attacked(
defended_attackers, framework
):
if not _any_attacker_is_attacked(defended_attackers, framework):
continue
for undefended in framework.arguments:
undefended_attackers = attackers[undefended]
if (
len(defended_attackers) == len(undefended_attackers)
and undefended_attackers
and not _any_attacker_is_attacked(undefended_attackers, framework)
and not result.strictly_prefers(defended, undefended)
):
Expand Down Expand Up @@ -212,28 +218,19 @@ def cardinality_precedence(
framework: ArgumentationFramework,
result: RankingResult,
) -> bool:
"""Check the fewer-unattacked-attackers postulate where applicable.
"""Check Bonzon et al. 2016 p. 2 cardinality precedence (CP).

Amgoud and Ben-Naim 2013 p. 8 and Bonzon et al. 2016 p. 1: when direct
attackers are all unattacked, fewer attackers strictly improves rank.
|R1-(a)| < |R1-(b)| implies a > b, for all arguments, including ones
with no attackers and regardless of whether the attackers are attacked.
"""

attackers = _attackers(framework)
unattacked = {argument for argument, values in attackers.items() if not values}
for left in framework.arguments:
left_attackers = attackers[left]
if not left_attackers or not left_attackers <= unattacked:
continue
for right in framework.arguments:
right_attackers = attackers[right]
if (
len(left_attackers) < len(right_attackers)
and right_attackers
and right_attackers <= unattacked
and not result.strictly_prefers(left, right)
):
return False
return True
return all(
result.strictly_prefers(left, right)
for left in framework.arguments
for right in framework.arguments
if len(attackers[left]) < len(attackers[right])
)


def quality_precedence(
Expand Down Expand Up @@ -441,16 +438,6 @@ def augment(item: str, seen: set[str]) -> bool:
return all(augment(item, set()) for item in items)


def _every_attacker_is_attacked(
attackers: frozenset[str],
framework: ArgumentationFramework,
) -> bool:
relation = _attack_relation(framework)
return all(
any(target == attacker for _, target in relation) for attacker in attackers
)


def _any_attacker_is_attacked(
attackers: frozenset[str],
framework: ArgumentationFramework,
Expand Down
18 changes: 18 additions & 0 deletions tests/ranking/test_ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,24 @@ def test_discussion_ranking_marks_a_bounded_cycle_as_truncated() -> None:
assert result.iterations == 4


def test_discussion_ranking_keeps_exact_counts_beyond_float_range() -> None:
"""Amgoud & Ben-Naim 2013, Defs. 10-11: Dis_i(a) counts linear
discussions exactly and Dbs compares the sequences lexicographically.
On the complete 3-cycle (9 edges) counts grow as 3^(i-1), which passes
the float range near i = 650; exact integer counts must not overflow."""
arguments = frozenset("abc")
framework = ArgumentationFramework(
arguments=arguments,
defeats=frozenset((a, b) for a in arguments for b in arguments),
)

result = discussion_based_ranking(framework, max_depth=650)

assert result.ranking == (arguments,)
assert result.converged is False
assert result.scores["a"][649] == 3**649


def test_counting_ranking_uses_normalized_alternating_path_counts() -> None:
# Delobelle--Villata 2019 page-004.png, Definition 5. With two leaf
# attackers, the matrix infinity norm is two and the attacked argument's
Expand Down
57 changes: 57 additions & 0 deletions tests/ranking/test_ranking_axioms.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,60 @@ def test_distributed_defense_precedence_prefers_spread_defense() -> None:
)

assert distributed_defense_precedence(framework, categoriser_ranking(framework))


def _all_tied(framework: ArgumentationFramework) -> RankingResult:
return RankingResult(
{argument: 0.0 for argument in framework.arguments},
(framework.arguments,),
True,
0,
"all-tied",
)


def test_self_contradiction_rejects_tie_with_self_attacker() -> None:
"""Bonzon et al. 2016, p. 2, SC: (a, a) not in R and (b, b) in R imply
a > b, strictly; a tie with the self-attacker violates it."""
framework = ArgumentationFramework(
arguments=frozenset({"a", "b"}), defeats=frozenset({("b", "b")})
)

assert self_contradiction(framework, _all_tied(framework)) is False


def test_cardinality_precedence_rejects_ties_on_a_chain() -> None:
"""Bonzon et al. 2016, p. 2, CP: |R1-(a)| < |R1-(b)| implies a > b, with
no requirement that attackers be unattacked and including zero attackers.
In a -> b -> c, a has fewer attackers than b and c, so ties violate CP."""
framework = ArgumentationFramework(
arguments=frozenset({"a", "b", "c"}),
defeats=frozenset({("a", "b"), ("b", "c")}),
)

assert cardinality_precedence(framework, _all_tied(framework)) is False
assert cardinality_precedence(framework, categoriser_ranking(framework))


def _amgoud_example_3() -> ArgumentationFramework:
return ArgumentationFramework(
arguments=frozenset("abcdegh"),
defeats=frozenset({("h", "c"), ("c", "a"), ("d", "a"), ("e", "b"), ("g", "b")}),
)


def test_defense_precedence_needs_only_a_nonempty_defender_set() -> None:
"""Amgoud & Ben-Naim 2013, Example 3, and Bonzon et al. 2016, p. 2, DP:
equal attacker counts, R2+(a) nonempty and R2+(b) empty imply a > b.
a is attacked by c (defended by h) and d (undefended); b by e and g;
tying a and b violates DP although not every attacker of a is attacked."""
framework = _amgoud_example_3()

assert defense_precedence(framework, _all_tied(framework)) is False


def test_defense_precedence_accepts_categoriser_on_example_3_control() -> None:
"""Control: the categoriser ranks a strictly above b on Example 3."""
framework = _amgoud_example_3()

assert defense_precedence(framework, categoriser_ranking(framework))
Loading