sorting_race/services/fairness/
weighted.rs

1//! Weighted fairness model implementation
2
3use crate::models::traits::{FairnessModel, Sorter};
4use std::collections::HashMap;
5
6/// Fairness model that allocates budgets based on α*comparisons + β*moves scoring
7/// Algorithms with lower weighted scores get more budget (inverse fairness)
8#[derive(Debug)]
9pub struct WeightedFairness {
10    alpha: f32,  // Weight for comparisons
11    beta: f32,   // Weight for moves
12    base_budget: usize,
13}
14
15impl WeightedFairness {
16    /// Create a new weighted fairness model
17    /// 
18    /// # Arguments
19    /// * `alpha` - Weight for comparisons in the scoring formula
20    /// * `beta` - Weight for moves in the scoring formula
21    pub fn new(alpha: f32, beta: f32) -> Self {
22        Self {
23            alpha,
24            beta, 
25            base_budget: 100, // Default base budget
26        }
27    }
28
29    /// Calculate weighted score for an algorithm: α*comparisons + β*moves
30    fn calculate_weighted_score(&self, algorithm: &dyn Sorter) -> f32 {
31        let telemetry = algorithm.get_telemetry();
32        self.alpha * telemetry.total_comparisons as f32 + self.beta * telemetry.total_moves as f32
33    }
34
35    /// Set the base budget for allocation
36    pub fn set_base_budget(&mut self, budget: usize) {
37        self.base_budget = budget.max(1);
38    }
39
40    /// Get the current base budget
41    pub fn get_base_budget(&self) -> usize {
42        self.base_budget
43    }
44}
45
46impl Default for WeightedFairness {
47    fn default() -> Self {
48        Self::new(1.0, 1.0) // Equal weights for comparisons and moves
49    }
50}
51
52impl FairnessModel for WeightedFairness {
53    fn allocate_budget(&self, algorithms: &[Box<dyn Sorter>]) -> Vec<usize> {
54        // Filter active algorithms
55        let active_algorithms: Vec<(usize, f32)> = algorithms
56            .iter()
57            .enumerate()
58            .filter_map(|(i, alg)| {
59                if alg.is_complete() {
60                    None
61                } else {
62                    Some((i, self.calculate_weighted_score(alg.as_ref())))
63                }
64            })
65            .collect();
66
67        // Handle edge cases
68        if active_algorithms.is_empty() {
69            return vec![0; algorithms.len()];
70        }
71
72        if active_algorithms.len() == 1 {
73            let mut budgets = vec![0; algorithms.len()];
74            budgets[active_algorithms[0].0] = self.base_budget;
75            return budgets;
76        }
77
78        // Handle zero weights case
79        if self.alpha == 0.0 && self.beta == 0.0 {
80            // Equal distribution when both weights are zero
81            let equal_budget = self.base_budget;
82            return algorithms
83                .iter()
84                .map(|alg| if alg.is_complete() { 0 } else { equal_budget })
85                .collect();
86        }
87
88        // Calculate total weighted scores for normalization
89        let total_score: f32 = active_algorithms.iter().map(|(_, score)| *score).sum();
90        
91        // Inverse fairness: algorithms with lower scores get MORE budget
92        let mut budgets = vec![0; algorithms.len()];
93        let total_budget = self.base_budget * active_algorithms.len();
94        
95        if total_score == 0.0 {
96            // If all scores are zero, distribute equally among active algorithms
97            let equal_budget = total_budget / active_algorithms.len();
98            for (idx, _) in active_algorithms {
99                budgets[idx] = equal_budget;
100            }
101        } else {
102            // Calculate inverse weights (lower score = higher budget)
103            for (idx, score) in &active_algorithms {
104                // Inverse proportion: (total - score) / total gives higher budget to lower scores
105                let max_score = active_algorithms.iter().map(|(_, s)| *s).fold(0.0f32, f32::max);
106                let inverse_score = max_score - score + 1.0; // Add 1 to avoid zero division
107                let total_inverse: f32 = active_algorithms.iter()
108                    .map(|(_, s)| max_score - s + 1.0)
109                    .sum();
110                
111                let budget = ((total_budget as f32 * inverse_score) / total_inverse).round() as usize;
112                budgets[*idx] = budget.max(1); // Ensure minimum budget
113            }
114        }
115
116        budgets
117    }
118
119    fn name(&self) -> &str {
120        "Weighted Fairness"
121    }
122}
123
124/// Performance-based weighted fairness that adjusts based on algorithm efficiency
125#[derive(Debug)]
126pub struct PerformanceWeightedFairness {
127    base_budget: usize,
128    efficiency_history: HashMap<String, Vec<f32>>,
129    history_window: usize,
130}
131
132impl PerformanceWeightedFairness {
133    /// Create a new performance-weighted fairness model
134    pub fn new(base_budget: usize, history_window: usize) -> Self {
135        Self {
136            base_budget: base_budget.max(1),
137            efficiency_history: HashMap::new(),
138            history_window: history_window.max(1),
139        }
140    }
141
142    /// Record efficiency for an algorithm
143    pub fn record_efficiency(&mut self, algorithm_name: &str, efficiency: f32) {
144        let history = self.efficiency_history
145            .entry(algorithm_name.to_string())
146            .or_default();
147        
148        history.push(efficiency.max(0.0));
149        
150        // Keep only recent history
151        if history.len() > self.history_window {
152            history.remove(0);
153        }
154    }
155
156    /// Calculate average efficiency for an algorithm
157    fn get_average_efficiency(&self, algorithm_name: &str) -> f32 {
158        if let Some(history) = self.efficiency_history.get(algorithm_name) {
159            if !history.is_empty() {
160                history.iter().sum::<f32>() / history.len() as f32
161            } else {
162                1.0 // Default efficiency
163            }
164        } else {
165            1.0 // Default efficiency for unknown algorithms
166        }
167    }
168
169    /// Clear efficiency history
170    pub fn clear_history(&mut self) {
171        self.efficiency_history.clear();
172    }
173}
174
175impl Default for PerformanceWeightedFairness {
176    fn default() -> Self {
177        Self::new(10, 10)
178    }
179}
180
181impl FairnessModel for PerformanceWeightedFairness {
182    fn allocate_budget(&self, algorithms: &[Box<dyn Sorter>]) -> Vec<usize> {
183        // Calculate total efficiency to normalize
184        let total_efficiency: f32 = algorithms
185            .iter()
186            .filter(|alg| !alg.is_complete())
187            .map(|alg| self.get_average_efficiency(alg.name()))
188            .sum();
189
190        if total_efficiency == 0.0 {
191            // Fallback to equal allocation
192            return algorithms
193                .iter()
194                .map(|alg| if alg.is_complete() { 0 } else { self.base_budget })
195                .collect();
196        }
197
198        algorithms
199            .iter()
200            .map(|algorithm| {
201                if algorithm.is_complete() {
202                    0
203                } else {
204                    let efficiency = self.get_average_efficiency(algorithm.name());
205                    let proportion = efficiency / total_efficiency;
206                    let weighted_budget = (self.base_budget as f32 * proportion * algorithms.len() as f32).round() as usize;
207                    weighted_budget.max(1)
208                }
209            })
210            .collect()
211    }
212
213    fn name(&self) -> &str {
214        "Performance Weighted"
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::services::sorters::{bubble::BubbleSort, quick::QuickSort};
222
223    #[test]
224    fn test_weighted_fairness_different_algorithms() {
225        let fairness = WeightedFairness::new(1.0, 1.0); // Equal weights
226        
227        let mut bubble = BubbleSort::new();
228        let mut quick = QuickSort::new();
229        bubble.reset(vec![3, 1, 2]);
230        quick.reset(vec![3, 1, 2]);
231
232        let algorithms: Vec<Box<dyn Sorter>> = vec![
233            Box::new(bubble),
234            Box::new(quick),
235        ];
236
237        let budgets = fairness.allocate_budget(&algorithms);
238        
239        // Both should get some budget
240        assert!(budgets[0] > 0);
241        assert!(budgets[1] > 0);
242        
243        // Total budget should be reasonable
244        let total: usize = budgets.iter().sum();
245        assert!(total > 0);
246    }
247
248    #[test]
249    fn test_performance_weighted_fairness() {
250        let mut fairness = PerformanceWeightedFairness::new(10, 5);
251        
252        // Record some efficiency data
253        fairness.record_efficiency("Algorithm A", 2.0);
254        fairness.record_efficiency("Algorithm A", 1.5);
255        fairness.record_efficiency("Algorithm B", 1.0);
256        
257        assert_eq!(fairness.get_average_efficiency("Algorithm A"), 1.75);
258        assert_eq!(fairness.get_average_efficiency("Algorithm B"), 1.0);
259    }
260
261    #[test]
262    fn test_weighted_fairness_alpha_beta_scoring() {
263        let fairness = WeightedFairness::new(2.0, 0.5); // Favor comparisons
264        
265        // Create mock algorithms with different stats
266        let mut high_comp = BubbleSort::new();
267        let mut high_move = BubbleSort::new();
268        high_comp.reset(vec![10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); // Larger reverse-sorted array
269        high_move.reset(vec![1, 3, 2, 4, 5, 7, 6, 8, 9, 10]); // Larger partially sorted
270        
271        // Simulate different amounts of work
272        high_comp.step(20); // Do more comparisons
273        high_move.step(5);  // Do fewer comparisons
274        
275        let algorithms: Vec<Box<dyn Sorter>> = vec![
276            Box::new(high_comp),
277            Box::new(high_move),
278        ];
279        
280        let budgets = fairness.allocate_budget(&algorithms);
281        
282        assert_eq!(budgets.len(), 2);
283        assert!(budgets[0] > 0, "First algorithm should get budget > 0, got {}", budgets[0]);
284        assert!(budgets[1] > 0, "Second algorithm should get budget > 0, got {}", budgets[1]);
285        
286        // The algorithm with fewer weighted operations should get more budget
287        let total: usize = budgets.iter().sum();
288        assert!(total > 0);
289    }
290}