argumentation_weighted_bipolar/
reduce.rs1use crate::error::Error;
5use crate::framework::WeightedBipolarFramework;
6use crate::types::Budget;
7use argumentation_bipolar::BipolarFramework;
8use std::fmt::Debug;
9use std::hash::Hash;
10
11fn cost_within_budget(cost: f64, budget: f64) -> bool {
21 if cost <= budget {
22 return true;
23 }
24 let scale = cost.abs().max(budget.abs());
25 if scale > 1e-100 {
26 cost - budget <= 1e-9 * scale
27 } else {
28 cost - budget <= 1e-100
29 }
30}
31
32pub const EDGE_ENUMERATION_LIMIT: usize = 24;
36
37pub fn wbipolar_residuals<A>(
43 framework: &WeightedBipolarFramework<A>,
44 budget: Budget,
45) -> Result<Vec<BipolarFramework<A>>, Error>
46where
47 A: Clone + Eq + Hash + Debug,
48{
49 let attacks: Vec<_> = framework.attacks().collect();
50 let supports: Vec<_> = framework.supports().collect();
51 let m_a = attacks.len();
52 let m_s = supports.len();
53 let m = m_a + m_s;
54
55 if m > EDGE_ENUMERATION_LIMIT {
56 return Err(Error::TooManyEdges {
57 edges: m,
58 limit: EDGE_ENUMERATION_LIMIT,
59 });
60 }
61
62 let args: Vec<A> = framework.arguments().cloned().collect();
63 let total = 1u64 << m;
64 let mut residuals = Vec::new();
65
66 for bits in 0..total {
67 let mut cost = 0.0_f64;
68 for (i, atk) in attacks.iter().enumerate() {
69 if bits & (1u64 << i) != 0 {
70 cost += atk.weight.value();
71 }
72 }
73 for (j, sup) in supports.iter().enumerate() {
74 if bits & (1u64 << (m_a + j)) != 0 {
75 cost += sup.weight.value();
76 }
77 }
78 if !cost_within_budget(cost, budget.value()) {
79 continue;
80 }
81
82 let mut bf: BipolarFramework<A> = BipolarFramework::new();
83 for a in &args {
84 bf.add_argument(a.clone());
85 }
86 for (i, atk) in attacks.iter().enumerate() {
87 if bits & (1u64 << i) == 0 {
88 bf.add_attack(atk.attacker.clone(), atk.target.clone());
89 }
90 }
91 for (j, sup) in supports.iter().enumerate() {
92 if bits & (1u64 << (m_a + j)) == 0 {
93 bf.add_support(sup.supporter.clone(), sup.supported.clone())?;
94 }
95 }
96 residuals.push(bf);
97 }
98
99 Ok(residuals)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn zero_budget_yields_single_residual_with_all_edges() {
108 let mut wbf = WeightedBipolarFramework::new();
109 wbf.add_weighted_attack("a", "b", 0.3).unwrap();
110 wbf.add_weighted_support("c", "a", 0.2).unwrap();
111 let residuals = wbipolar_residuals(&wbf, Budget::zero()).unwrap();
112 assert_eq!(residuals.len(), 1);
113 let r = &residuals[0];
114 assert_eq!(r.attacks().count(), 1);
115 assert_eq!(r.supports().count(), 1);
116 }
117
118 #[test]
119 fn large_budget_yields_full_power_set_over_edges() {
120 let mut wbf = WeightedBipolarFramework::new();
121 wbf.add_weighted_attack("a", "b", 0.5).unwrap();
122 wbf.add_weighted_support("c", "a", 0.5).unwrap();
123 let residuals = wbipolar_residuals(&wbf, Budget::new(10.0).unwrap()).unwrap();
124 assert_eq!(residuals.len(), 4);
125 }
126
127 #[test]
128 fn budget_at_cheapest_edge_yields_two_residuals() {
129 let mut wbf = WeightedBipolarFramework::new();
130 wbf.add_weighted_attack("a", "b", 0.2).unwrap();
131 wbf.add_weighted_support("c", "a", 0.9).unwrap();
132 let residuals = wbipolar_residuals(&wbf, Budget::new(0.2).unwrap()).unwrap();
133 assert_eq!(residuals.len(), 2);
134 }
135
136 #[test]
137 fn oversized_framework_rejected() {
138 let mut wbf: WeightedBipolarFramework<u32> = WeightedBipolarFramework::new();
139 for i in 0..(EDGE_ENUMERATION_LIMIT as u32 + 1) {
140 wbf.add_weighted_attack(2 * i, 2 * i + 1, 0.1).unwrap();
141 }
142 let err = wbipolar_residuals(&wbf, Budget::new(1.0).unwrap()).unwrap_err();
143 assert!(matches!(err, Error::TooManyEdges { .. }));
144 }
145
146 #[test]
147 fn every_residual_preserves_all_arguments() {
148 let mut wbf = WeightedBipolarFramework::new();
149 wbf.add_argument("isolated");
150 wbf.add_weighted_attack("a", "b", 0.5).unwrap();
151 let residuals = wbipolar_residuals(&wbf, Budget::new(1.0).unwrap()).unwrap();
152 for r in &residuals {
153 assert_eq!(r.arguments().count(), 3);
154 }
155 }
156
157 #[test]
158 fn budget_tolerates_floating_point_sum_of_intended_weights() {
159 let mut wbf = WeightedBipolarFramework::new();
163 wbf.add_weighted_attack("a", "b", 0.1).unwrap();
164 wbf.add_weighted_support("c", "a", 0.2).unwrap();
165 let residuals = wbipolar_residuals(&wbf, Budget::new(0.3).unwrap()).unwrap();
166 let dropped_both = residuals
168 .iter()
169 .any(|bf| bf.attacks().count() == 0 && bf.supports().count() == 0);
170 assert!(
171 dropped_both,
172 "expected to find residual with both edges dropped at budget 0.3"
173 );
174 }
175}