Skip to main content

argumentation_bipolar/
queries.rs

1//! Transitive queries over a bipolar framework.
2//!
3//! - [`transitive_supporters`] — all arguments that directly or
4//!   indirectly support `a` via the support graph.
5//! - [`transitive_attackers`] — all arguments that attack `a` under the
6//!   closed attack relation (direct + derived).
7//! - [`coalitioned_with`] — the members of `a`'s coalition per
8//!   [`crate::coalition::detect_coalitions`].
9
10use crate::coalition::detect_coalitions;
11use crate::derived::closed_attacks;
12use crate::framework::BipolarFramework;
13use std::collections::{HashSet, VecDeque};
14use std::hash::Hash;
15
16/// All arguments that directly or transitively support `a` in the
17/// support graph. Does not include `a` itself.
18#[must_use]
19pub fn transitive_supporters<A>(framework: &BipolarFramework<A>, a: &A) -> HashSet<A>
20where
21    A: Clone + Eq + Hash,
22{
23    let mut visited: HashSet<A> = HashSet::new();
24    let mut frontier: VecDeque<A> = VecDeque::new();
25    frontier.push_back(a.clone());
26
27    while let Some(current) = frontier.pop_front() {
28        for (supporter, supported) in framework.supports() {
29            if *supported == current && visited.insert(supporter.clone()) {
30                frontier.push_back(supporter.clone());
31            }
32        }
33    }
34    visited.remove(a);
35    visited
36}
37
38/// All arguments that attack `a` under the closed attack relation
39/// (direct attacks plus derived attacks from support closure).
40#[must_use]
41pub fn transitive_attackers<A>(framework: &BipolarFramework<A>, a: &A) -> HashSet<A>
42where
43    A: Clone + Eq + Hash,
44{
45    closed_attacks(framework)
46        .into_iter()
47        .filter_map(|(att, tgt)| if tgt == *a { Some(att) } else { None })
48        .collect()
49}
50
51/// The members of `a`'s coalition, excluding `a` itself. If `a` is in
52/// a singleton coalition, returns an empty set.
53#[must_use]
54pub fn coalitioned_with<A>(framework: &BipolarFramework<A>, a: &A) -> HashSet<A>
55where
56    A: Clone + Eq + Hash,
57{
58    let coalitions = detect_coalitions(framework);
59    for coalition in coalitions {
60        if coalition.members.contains(a) {
61            return coalition.members.into_iter().filter(|m| m != a).collect();
62        }
63    }
64    HashSet::new()
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn transitive_supporters_walks_support_chain() {
73        // a supports b, b supports c. Transitive supporters of c: {a, b}.
74        let mut bf = BipolarFramework::new();
75        bf.add_support("a", "b").unwrap();
76        bf.add_support("b", "c").unwrap();
77        let sups = transitive_supporters(&bf, &"c");
78        assert_eq!(sups.len(), 2);
79        assert!(sups.contains(&"a"));
80        assert!(sups.contains(&"b"));
81    }
82
83    #[test]
84    fn transitive_attackers_includes_derived_edges() {
85        // a supports x, x attacks b ⇒ a attacks b (derived).
86        let mut bf = BipolarFramework::new();
87        bf.add_support("a", "x").unwrap();
88        bf.add_attack("x", "b");
89        let atts = transitive_attackers(&bf, &"b");
90        assert!(atts.contains(&"a"), "derived attacker should be present");
91        assert!(atts.contains(&"x"));
92    }
93
94    #[test]
95    fn coalitioned_with_returns_siblings() {
96        let mut bf = BipolarFramework::new();
97        bf.add_support("alice", "bob").unwrap();
98        bf.add_support("bob", "alice").unwrap();
99        let allies = coalitioned_with(&bf, &"alice");
100        assert_eq!(allies.len(), 1);
101        assert!(allies.contains(&"bob"));
102    }
103
104    #[test]
105    fn coalitioned_with_returns_empty_for_singleton() {
106        let mut bf = BipolarFramework::new();
107        bf.add_argument("alice");
108        let allies = coalitioned_with(&bf, &"alice");
109        assert!(allies.is_empty());
110    }
111}