Skip to main content

argumentation_weighted/
semantics.rs

1//! β-acceptance under Dunne 2011 inconsistency-budget semantics.
2//!
3//! All entry points iterate every β-inconsistent residual produced by
4//! [`crate::reduce::dunne_residuals`] and aggregate across them:
5//! **credulous** queries take an OR (exists-residual), **skeptical**
6//! queries take an AND (forall-residual), and extension queries return
7//! the set-union of all per-residual extensions.
8
9use crate::error::Error;
10use crate::framework::WeightedFramework;
11use crate::reduce::dunne_residuals;
12use crate::types::Budget;
13use std::collections::HashSet;
14use std::fmt::Debug;
15use std::hash::Hash;
16
17/// Union of grounded extensions across all β-inconsistent residuals.
18/// Matches Dunne 2011's credulous reading for the grounded semantics.
19pub fn grounded_at_budget<A>(
20    framework: &WeightedFramework<A>,
21    budget: Budget,
22) -> Result<HashSet<A>, Error>
23where
24    A: Clone + Eq + Hash + Debug + Ord,
25{
26    let mut union: HashSet<A> = HashSet::new();
27    for af in dunne_residuals(framework, budget)? {
28        union.extend(af.grounded_extension());
29    }
30    Ok(union)
31}
32
33/// Union of complete extensions across all β-inconsistent residuals.
34pub fn complete_at_budget<A>(
35    framework: &WeightedFramework<A>,
36    budget: Budget,
37) -> Result<Vec<HashSet<A>>, Error>
38where
39    A: Clone + Eq + Hash + Debug + Ord,
40{
41    let mut out: Vec<HashSet<A>> = Vec::new();
42    for af in dunne_residuals(framework, budget)? {
43        for ext in af.complete_extensions()? {
44            if !out.contains(&ext) {
45                out.push(ext);
46            }
47        }
48    }
49    Ok(out)
50}
51
52/// Union of preferred extensions across all β-inconsistent residuals.
53pub fn preferred_at_budget<A>(
54    framework: &WeightedFramework<A>,
55    budget: Budget,
56) -> Result<Vec<HashSet<A>>, Error>
57where
58    A: Clone + Eq + Hash + Debug + Ord,
59{
60    let mut out: Vec<HashSet<A>> = Vec::new();
61    for af in dunne_residuals(framework, budget)? {
62        for ext in af.preferred_extensions()? {
63            if !out.contains(&ext) {
64                out.push(ext);
65            }
66        }
67    }
68    Ok(out)
69}
70
71/// Union of stable extensions across all β-inconsistent residuals. A
72/// residual may have no stable extensions; those contribute nothing.
73pub fn stable_at_budget<A>(
74    framework: &WeightedFramework<A>,
75    budget: Budget,
76) -> Result<Vec<HashSet<A>>, Error>
77where
78    A: Clone + Eq + Hash + Debug + Ord,
79{
80    let mut out: Vec<HashSet<A>> = Vec::new();
81    for af in dunne_residuals(framework, budget)? {
82        for ext in af.stable_extensions()? {
83            if !out.contains(&ext) {
84                out.push(ext);
85            }
86        }
87    }
88    Ok(out)
89}
90
91/// β-credulous acceptance: `target` appears in some preferred extension
92/// of some β-inconsistent residual.
93pub fn is_credulously_accepted_at<A>(
94    framework: &WeightedFramework<A>,
95    target: &A,
96    budget: Budget,
97) -> Result<bool, Error>
98where
99    A: Clone + Eq + Hash + Debug + Ord,
100{
101    for af in dunne_residuals(framework, budget)? {
102        if af.preferred_extensions()?.iter().any(|e| e.contains(target)) {
103            return Ok(true);
104        }
105    }
106    Ok(false)
107}
108
109/// β-skeptical acceptance: `target` appears in every preferred
110/// extension of every β-inconsistent residual. Returns `false` for
111/// frameworks with no preferred extensions in any residual.
112pub fn is_skeptically_accepted_at<A>(
113    framework: &WeightedFramework<A>,
114    target: &A,
115    budget: Budget,
116) -> Result<bool, Error>
117where
118    A: Clone + Eq + Hash + Debug + Ord,
119{
120    // dunne_residuals always yields at least the empty-subset
121    // residual (cost 0 ≤ any non-negative β), so the list is never
122    // empty in practice. We still guard against `exts.is_empty()`
123    // per residual because a Dung framework with certain cyclic
124    // attacks can have no preferred extensions.
125    for af in dunne_residuals(framework, budget)? {
126        let exts = af.preferred_extensions()?;
127        if exts.is_empty() {
128            return Ok(false);
129        }
130        if !exts.iter().all(|e| e.contains(target)) {
131            return Ok(false);
132        }
133    }
134    Ok(true)
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn grounded_at_zero_budget_matches_dung() {
143        let mut wf = WeightedFramework::new();
144        wf.add_weighted_attack("a", "b", 0.5).unwrap();
145        wf.add_weighted_attack("b", "c", 0.5).unwrap();
146        let grounded = grounded_at_budget(&wf, Budget::zero()).unwrap();
147        assert!(grounded.contains(&"a"));
148        assert!(grounded.contains(&"c"));
149        assert!(!grounded.contains(&"b"));
150    }
151
152    #[test]
153    fn grounded_union_widens_as_budget_grows() {
154        let mut wf = WeightedFramework::new();
155        wf.add_weighted_attack("a", "b", 0.5).unwrap();
156        let g0 = grounded_at_budget(&wf, Budget::zero()).unwrap();
157        let g1 = grounded_at_budget(&wf, Budget::new(1.0).unwrap()).unwrap();
158        // At β=0: grounded = {a}. At β=1 (both residuals {} and {a→b}):
159        // union = {a, b}.
160        assert!(g0.is_subset(&g1));
161        assert!(g1.contains(&"b"));
162    }
163
164    #[test]
165    fn credulous_acceptance_monotone_in_budget() {
166        let mut wf = WeightedFramework::new();
167        wf.add_weighted_attack("a", "b", 0.3).unwrap();
168        wf.add_weighted_attack("b", "c", 0.7).unwrap();
169        // c should flip from true (at β=0, defended by a) to stay true
170        // (at β=0.3, still defended), and b should flip from false to
171        // true at β=0.3 (a→b can be tolerated).
172        let at0 = is_credulously_accepted_at(&wf, &"b", Budget::zero()).unwrap();
173        let at03 = is_credulously_accepted_at(&wf, &"b", Budget::new(0.3).unwrap()).unwrap();
174        assert!(!at0);
175        assert!(at03);
176    }
177
178    #[test]
179    fn skeptical_true_for_grounded_singleton() {
180        let mut wf = WeightedFramework::new();
181        wf.add_weighted_attack("a", "b", 0.5).unwrap();
182        // β = 0: unique preferred extension = {a}. Skeptical: a ∈ every
183        // extension of every residual (only residual is {a→b}).
184        assert!(is_skeptically_accepted_at(&wf, &"a", Budget::zero()).unwrap());
185        assert!(!is_skeptically_accepted_at(&wf, &"b", Budget::zero()).unwrap());
186    }
187
188    #[test]
189    fn preferred_at_budget_is_union_across_residuals() {
190        let mut wf = WeightedFramework::new();
191        wf.add_weighted_attack("a", "b", 0.2).unwrap();
192        wf.add_weighted_attack("b", "c", 0.4).unwrap();
193        let at0 = preferred_at_budget(&wf, Budget::zero()).unwrap();
194        assert!(at0.iter().any(|e| e.contains("a") && e.contains("c")));
195
196        let at02 = preferred_at_budget(&wf, Budget::new(0.2).unwrap()).unwrap();
197        let union: std::collections::HashSet<&str> =
198            at02.iter().flat_map(|e| e.iter().copied()).collect();
199        assert!(union.contains("b"), "b should be reachable at β=0.2");
200        assert!(union.contains("c"));
201    }
202
203    #[test]
204    fn preferred_at_budget_large_enough_accepts_all() {
205        let mut wf = WeightedFramework::new();
206        wf.add_weighted_attack("a", "b", 0.5).unwrap();
207        let at_big = preferred_at_budget(&wf, Budget::new(10.0).unwrap()).unwrap();
208        let union: std::collections::HashSet<&str> =
209            at_big.iter().flat_map(|e| e.iter().copied()).collect();
210        assert!(union.contains("a"));
211        assert!(union.contains("b"));
212    }
213}