Skip to main content

qualia_core_db/modalities/argumentation/
bipolar.rs

1//! Bipolar Argumentation (Cayrol & Lagasquie-Schiex) — adds a **support** relation alongside
2//! attack and derives the *complex attacks* it induces (deductive-support semantics):
3//!   * **supported attack:**  a supports* b  ∧  b attacks c   ⇒   a attacks c
4//!   * **secondary attack:**  a attacks b    ∧  c supports* b  ⇒   a attacks c
5//! Projecting these derived attacks back into a Dung framework lets the standard semantics apply.
6
7use super::{ArgumentationFramework, Attack, AttackType};
8use std::collections::HashSet;
9
10/// A Dung framework augmented with a binary support relation.
11#[derive(Debug, Clone)]
12pub struct BipolarFramework {
13    pub af: ArgumentationFramework,
14    /// `(supporter, supported)` edges.
15    pub supports: Vec<(u64, u64)>,
16}
17
18impl BipolarFramework {
19    pub fn new(af: ArgumentationFramework) -> Self {
20        Self {
21            af,
22            supports: Vec::new(),
23        }
24    }
25
26    /// Add a support edge `supporter → supported`.
27    pub fn add_support(&mut self, supporter: u64, supported: u64) {
28        self.supports.push((supporter, supported));
29    }
30
31    /// Is there a support path `from →…→ to` of length ≥ 1?
32    pub fn support_reaches(&self, from: u64, to: u64) -> bool {
33        let mut stack = vec![from];
34        let mut seen = HashSet::new();
35        seen.insert(from);
36        while let Some(x) = stack.pop() {
37            for &(s, t) in &self.supports {
38                if s == x {
39                    if t == to {
40                        return true;
41                    }
42                    if seen.insert(t) {
43                        stack.push(t);
44                    }
45                }
46            }
47        }
48        false
49    }
50
51    /// Project to a Dung framework whose attacks are the original attacks PLUS the derived
52    /// supported and secondary attacks. Duplicate edges are harmless to the semantics.
53    pub fn to_dung(&self) -> ArgumentationFramework {
54        let mk = |a: u64, b: u64| Attack {
55            attacker: a,
56            target: b,
57            attack_type: AttackType::Rebuttal,
58            strength: 1.0,
59        };
60        let mut out = ArgumentationFramework::new();
61        for arg in self.af.arguments.values() {
62            out.add_argument(arg.clone());
63        }
64        for atk in &self.af.attacks {
65            out.add_attack(atk.clone());
66        }
67        let ids: Vec<u64> = self.af.arguments.keys().copied().collect();
68
69        for atk in &self.af.attacks {
70            let (b, c) = (atk.attacker, atk.target);
71            // supported attack: a supports* b ∧ b attacks c ⇒ a attacks c
72            for &a in &ids {
73                if a != b && self.support_reaches(a, b) {
74                    out.add_attack(mk(a, c));
75                }
76            }
77            // secondary attack: a attacks b ∧ c supports* b ⇒ a attacks c
78            let (a, bb) = (atk.attacker, atk.target);
79            for &c in &ids {
80                if c != bb && self.support_reaches(c, bb) {
81                    out.add_attack(mk(a, c));
82                }
83            }
84        }
85        out
86    }
87
88    /// Grounded extension over the derived (complex-attack) Dung framework.
89    pub fn grounded_extension(&self) -> HashSet<u64> {
90        self.to_dung().grounded_extension()
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::super::{Argument, Attack, AttackType};
97    use super::*;
98    use crate::NQuin;
99
100    fn arg(id: u64) -> Argument {
101        Argument::new(id, String::new(), Vec::new(), NQuin::default())
102    }
103
104    #[test]
105    fn support_induces_a_supported_attack() {
106        // a supports b; b attacks c. Deductive support ⇒ a attacks c.
107        let mut af = ArgumentationFramework::new();
108        af.add_argument(arg(1)); // a
109        af.add_argument(arg(2)); // b
110        af.add_argument(arg(3)); // c
111        af.add_attack(Attack {
112            attacker: 2,
113            target: 3,
114            attack_type: AttackType::Rebuttal,
115            strength: 1.0,
116        });
117
118        let mut bf = BipolarFramework::new(af);
119        bf.add_support(1, 2); // a supports b
120        assert!(bf.support_reaches(1, 2));
121
122        let dung = bf.to_dung();
123        // The derived framework contains a→c (the supported attack).
124        assert!(
125            dung.attacks
126                .iter()
127                .any(|atk| atk.attacker == 1 && atk.target == 3),
128            "a supports b, b attacks c ⇒ a attacks c"
129        );
130        // c has attackers {2,3-side}; with a supporting b, c is not accepted.
131        let g = bf.grounded_extension();
132        assert!(
133            !g.contains(&3),
134            "c is defeated through the support-backed attack"
135        );
136    }
137
138    #[test]
139    fn support_paths_are_transitive() {
140        let af = {
141            let mut a = ArgumentationFramework::new();
142            a.add_argument(arg(1));
143            a.add_argument(arg(2));
144            a.add_argument(arg(3));
145            a
146        };
147        let mut bf = BipolarFramework::new(af);
148        bf.add_support(1, 2);
149        bf.add_support(2, 3);
150        assert!(bf.support_reaches(1, 3), "support is transitive (1→2→3)");
151        assert!(!bf.support_reaches(3, 1));
152    }
153}