Skip to main content

argumentation_bipolar/
semantics.rs

1//! Bipolar semantics: compute Dung extensions on the flattened framework
2//! and filter them for support closure under necessary-support semantics.
3//!
4//! An extension `E` is **support-closed** iff for every `a ∈ E`, every
5//! direct necessary supporter of `a` is also in `E`. Nouioua & Risch 2011
6//! proves this captures necessary-support acceptability exactly when
7//! applied on top of Dung extensions of the closed attack relation.
8//!
9//! # Emptiness caveat
10//!
11//! The Dung-preferred-then-filter pipeline means that if every Dung-
12//! preferred extension fails support-closure, the public
13//! [`bipolar_preferred_extensions`] returns an empty `Vec`, even when
14//! strictly smaller support-closed admissible sets exist. This is a
15//! deliberate scoping choice for v0.1 — a future release may add a
16//! companion function that relaxes Dung-preferredness in favour of
17//! maximal support-closed admissibility. Consumers that need a
18//! guaranteed non-empty result on any framework should either:
19//!
20//! 1. Use [`bipolar_grounded_extension`] (always single and non-empty in
21//!    the trivial sense), or
22//! 2. Check `is_empty()` on the result and fall back to grounded.
23
24use crate::error::Error;
25use crate::flatten::flatten;
26use crate::framework::BipolarFramework;
27use std::collections::HashSet;
28use std::fmt::Debug;
29use std::hash::Hash;
30
31/// Check whether a candidate extension is support-closed in a bipolar
32/// framework: every argument in the extension has all its direct
33/// necessary supporters in the extension too.
34#[must_use]
35pub fn is_support_closed<A>(framework: &BipolarFramework<A>, extension: &HashSet<A>) -> bool
36where
37    A: Clone + Eq + Hash,
38{
39    for a in extension {
40        for supporter in framework.direct_supporters(a) {
41            if !extension.contains(supporter) {
42                return false;
43            }
44        }
45    }
46    true
47}
48
49/// All bipolar preferred extensions under necessary-support semantics.
50///
51/// Pipeline: flatten → Dung preferred extensions → support-closure filter.
52/// The filter may drop candidates that are Dung-preferred but not
53/// support-closed. It does NOT promote smaller subsets in their place;
54/// if every Dung-preferred extension is filtered out, the result is
55/// empty.
56pub fn bipolar_preferred_extensions<A>(
57    framework: &BipolarFramework<A>,
58) -> Result<Vec<HashSet<A>>, Error>
59where
60    A: Clone + Eq + Hash + Debug + Ord,
61{
62    let af = flatten(framework)?;
63    let dung_preferred = af.preferred_extensions()?;
64    let filtered: Vec<HashSet<A>> = dung_preferred
65        .into_iter()
66        .filter(|ext| is_support_closed(framework, ext))
67        .collect();
68    Ok(filtered)
69}
70
71/// All bipolar complete extensions under necessary-support semantics.
72pub fn bipolar_complete_extensions<A>(
73    framework: &BipolarFramework<A>,
74) -> Result<Vec<HashSet<A>>, Error>
75where
76    A: Clone + Eq + Hash + Debug + Ord,
77{
78    let af = flatten(framework)?;
79    let dung_complete = af.complete_extensions()?;
80    let filtered: Vec<HashSet<A>> = dung_complete
81        .into_iter()
82        .filter(|ext| is_support_closed(framework, ext))
83        .collect();
84    Ok(filtered)
85}
86
87/// All bipolar stable extensions.
88pub fn bipolar_stable_extensions<A>(
89    framework: &BipolarFramework<A>,
90) -> Result<Vec<HashSet<A>>, Error>
91where
92    A: Clone + Eq + Hash + Debug + Ord,
93{
94    let af = flatten(framework)?;
95    let dung_stable = af.stable_extensions()?;
96    let filtered: Vec<HashSet<A>> = dung_stable
97        .into_iter()
98        .filter(|ext| is_support_closed(framework, ext))
99        .collect();
100    Ok(filtered)
101}
102
103/// The bipolar grounded extension.
104///
105/// The Dung grounded extension is unique and may or may not be
106/// support-closed. If it is not, this function returns the largest
107/// support-closed subset of it — specifically, the result of
108/// iteratively removing any argument whose direct supporter is missing.
109/// (This differs from the preferred/complete case where we drop the
110/// whole candidate; grounded is unique so we must always return
111/// something.)
112pub fn bipolar_grounded_extension<A>(framework: &BipolarFramework<A>) -> Result<HashSet<A>, Error>
113where
114    A: Clone + Eq + Hash + Debug + Ord,
115{
116    let af = flatten(framework)?;
117    let mut grounded = af.grounded_extension();
118    // Support-closure repair: remove arguments whose direct supporters
119    // are missing, until a fixed point.
120    loop {
121        let to_remove: Vec<A> = grounded
122            .iter()
123            .filter(|a| {
124                framework
125                    .direct_supporters(a)
126                    .iter()
127                    .any(|s| !grounded.contains(*s))
128            })
129            .cloned()
130            .collect();
131        if to_remove.is_empty() {
132            break;
133        }
134        for a in to_remove {
135            grounded.remove(&a);
136        }
137    }
138    Ok(grounded)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn empty_extension_is_support_closed() {
147        let bf: BipolarFramework<&str> = BipolarFramework::new();
148        let ext: HashSet<&str> = HashSet::new();
149        assert!(is_support_closed(&bf, &ext));
150    }
151
152    #[test]
153    fn extension_missing_supporter_fails_closure() {
154        let mut bf = BipolarFramework::new();
155        bf.add_support("a", "b").unwrap();
156        let ext: HashSet<&str> = ["b"].into_iter().collect();
157        assert!(!is_support_closed(&bf, &ext));
158    }
159
160    #[test]
161    fn extension_containing_supporter_passes_closure() {
162        let mut bf = BipolarFramework::new();
163        bf.add_support("a", "b").unwrap();
164        let ext: HashSet<&str> = ["a", "b"].into_iter().collect();
165        assert!(is_support_closed(&bf, &ext));
166    }
167
168    #[test]
169    fn bipolar_preferred_filters_unsupported_candidates() {
170        // b has a necessary supporter a, but nothing attacks either.
171        // Dung would give {a, b} as the only preferred extension.
172        // Bipolar should give the same (and no {b}-alone candidate).
173        let mut bf = BipolarFramework::new();
174        bf.add_support("a", "b").unwrap();
175        let prefs = bipolar_preferred_extensions(&bf).unwrap();
176        assert_eq!(prefs.len(), 1);
177        assert_eq!(prefs[0].len(), 2);
178        assert!(prefs[0].contains(&"a"));
179        assert!(prefs[0].contains(&"b"));
180    }
181
182    #[test]
183    fn grounded_repair_removes_unsupported_arguments() {
184        // a is attacked by c, b is supported by a. Dung grounded would
185        // include b (unattacked) but not a (attacked by unattacked c).
186        // Under support closure, b must be removed because its
187        // supporter a is missing.
188        let mut bf = BipolarFramework::new();
189        bf.add_support("a", "b").unwrap();
190        bf.add_attack("c", "a");
191        let grounded = bipolar_grounded_extension(&bf).unwrap();
192        assert!(grounded.contains(&"c"));
193        assert!(!grounded.contains(&"a"));
194        assert!(
195            !grounded.contains(&"b"),
196            "b should be removed because its supporter a is missing"
197        );
198    }
199}