diff --git a/src/argumentation/ranking/ranking.py b/src/argumentation/ranking/ranking.py index 273be45..f545309 100644 --- a/src/argumentation/ranking/ranking.py +++ b/src/argumentation/ranking/ranking.py @@ -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]: diff --git a/src/argumentation/ranking/ranking_axioms.py b/src/argumentation/ranking/ranking_axioms.py index 9424951..b09856c 100644 --- a/src/argumentation/ranking/ranking_axioms.py +++ b/src/argumentation/ranking/ranking_axioms.py @@ -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 @@ -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 ) @@ -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) ): @@ -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( @@ -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, diff --git a/tests/ranking/test_ranking.py b/tests/ranking/test_ranking.py index c846ab2..5097b1a 100644 --- a/tests/ranking/test_ranking.py +++ b/tests/ranking/test_ranking.py @@ -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 diff --git a/tests/ranking/test_ranking_axioms.py b/tests/ranking/test_ranking_axioms.py index 6c4a64b..72ed3d2 100644 --- a/tests/ranking/test_ranking_axioms.py +++ b/tests/ranking/test_ranking_axioms.py @@ -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))