sorting_race/services/fairness/
adaptive.rs

1//! Adaptive fairness model that adjusts allocation based on algorithm progress rates
2
3use crate::models::traits::{FairnessModel, Sorter};
4use std::collections::HashMap;
5use std::cell::RefCell;
6
7/// Adaptive fairness model that learns algorithm efficiency and allocates budget accordingly
8/// Uses exponential moving average to track progress rates and allocate budget to slower algorithms
9#[derive(Debug)]
10pub struct AdaptiveFairness {
11    learning_rate: f32,
12    base_budget: usize,
13    progress_rates: RefCell<HashMap<String, f32>>, // Exponential moving average of progress rates
14    previous_progress: RefCell<HashMap<String, f32>>, // Previous progress values for rate calculation
15    step_count: RefCell<HashMap<String, u64>>, // Number of steps taken by each algorithm
16}
17
18impl AdaptiveFairness {
19    /// Create a new adaptive fairness model
20    /// 
21    /// # Arguments
22    /// * `learning_rate` - How quickly to adapt to performance changes (0.0 to 1.0)
23    pub fn new(learning_rate: f32) -> Self {
24        Self {
25            learning_rate: learning_rate.clamp(0.0, 1.0),
26            base_budget: 100,
27            progress_rates: RefCell::new(HashMap::new()),
28            previous_progress: RefCell::new(HashMap::new()),
29            step_count: RefCell::new(HashMap::new()),
30        }
31    }
32
33    /// Update progress rate for an algorithm using exponential moving average
34    fn update_progress_rate(&self, algorithm: &dyn Sorter) {
35        let name = algorithm.name();
36        let current_progress = algorithm.get_telemetry().progress_hint;
37        
38        let mut progress_rates = self.progress_rates.borrow_mut();
39        let mut previous_progress = self.previous_progress.borrow_mut();
40        let mut step_count = self.step_count.borrow_mut();
41        
42        // Get previous progress or initialize to 0.0
43        let prev_progress = previous_progress.get(name).copied().unwrap_or(0.0);
44        
45        // Calculate progress rate (progress change per step)
46        let progress_delta = current_progress - prev_progress;
47        let current_rate = progress_delta.max(0.0); // Only positive progress
48        
49        // Update exponential moving average
50        // Formula: new_rate = (1 - learning_rate) * old_rate + learning_rate * current_rate
51        let old_rate = progress_rates.get(name).copied().unwrap_or(current_rate);
52        let new_rate = (1.0 - self.learning_rate) * old_rate + self.learning_rate * current_rate;
53        
54        progress_rates.insert(name.to_string(), new_rate);
55        previous_progress.insert(name.to_string(), current_progress);
56        
57        // Increment step count
58        let steps = step_count.get(name).copied().unwrap_or(0) + 1;
59        step_count.insert(name.to_string(), steps);
60    }
61
62    /// Get the current progress rate for an algorithm
63    fn get_progress_rate(&self, algorithm_name: &str) -> f32 {
64        self.progress_rates.borrow().get(algorithm_name).copied().unwrap_or(0.01) // Default small rate
65    }
66
67    /// Set the base budget
68    pub fn set_base_budget(&mut self, budget: usize) {
69        self.base_budget = budget.max(1);
70    }
71
72    /// Clear all progress tracking data
73    pub fn clear_history(&self) {
74        self.progress_rates.borrow_mut().clear();
75        self.previous_progress.borrow_mut().clear();
76        self.step_count.borrow_mut().clear();
77    }
78
79    /// Get learning rate
80    pub fn get_learning_rate(&self) -> f32 {
81        self.learning_rate
82    }
83}
84
85impl Default for AdaptiveFairness {
86    fn default() -> Self {
87        Self::new(0.2) // 20% learning rate
88    }
89}
90
91impl FairnessModel for AdaptiveFairness {
92    fn allocate_budget(&self, algorithms: &[Box<dyn Sorter>]) -> Vec<usize> {
93        // Update progress rates for all algorithms
94        for algorithm in algorithms.iter() {
95            if !algorithm.is_complete() {
96                self.update_progress_rate(algorithm.as_ref());
97            }
98        }
99        
100        // Filter active algorithms and their progress rates
101        let active_algos: Vec<(usize, f32)> = algorithms
102            .iter()
103            .enumerate()
104            .filter_map(|(i, alg)| {
105                if alg.is_complete() {
106                    None
107                } else {
108                    let rate = self.get_progress_rate(alg.name());
109                    Some((i, rate))
110                }
111            })
112            .collect();
113        
114        // Handle edge cases
115        if active_algos.is_empty() {
116            return vec![0; algorithms.len()];
117        }
118        
119        if active_algos.len() == 1 {
120            let mut budgets = vec![0; algorithms.len()];
121            budgets[active_algos[0].0] = self.base_budget;
122            return budgets;
123        }
124        
125        // Calculate total budget to distribute
126        let total_budget = self.base_budget * active_algos.len();
127        
128        // Calculate inverse fairness: algorithms with slower progress get more help
129        let max_rate = active_algos.iter().map(|(_, rate)| *rate).fold(0.0f32, f32::max);
130        let min_rate = active_algos.iter().map(|(_, rate)| *rate).fold(f32::INFINITY, f32::min);
131        
132        let mut budgets = vec![0; algorithms.len()];
133        
134        if max_rate == min_rate || max_rate == 0.0 {
135            // All algorithms have same progress rate, distribute equally
136            let equal_budget = total_budget / active_algos.len();
137            for (idx, _) in active_algos {
138                budgets[idx] = equal_budget;
139            }
140        } else {
141            // Give more budget to algorithms with slower progress (need more help)
142            let total_inverse: f32 = active_algos.iter()
143                .map(|(_, rate)| max_rate - rate + 0.01) // Add small epsilon to avoid zero
144                .sum();
145            
146            for (idx, rate) in active_algos {
147                let inverse_weight = max_rate - rate + 0.01;
148                let proportion = inverse_weight / total_inverse;
149                let budget = ((total_budget as f32 * proportion).round() as usize).max(1);
150                budgets[idx] = budget;
151            }
152        }
153        
154        budgets
155    }
156    
157    fn name(&self) -> &str {
158        "Adaptive Fairness"
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::models::traits::Telemetry;
166    
167    #[derive(Debug)]
168    struct MockSorter {
169        name: String,
170        complete: bool,
171        progress: f32,
172    }
173    
174    impl MockSorter {
175        fn new(name: &str, progress: f32) -> Self {
176            Self {
177                name: name.to_string(),
178                complete: false,
179                progress,
180            }
181        }
182    }
183    
184    impl Sorter for MockSorter {
185        fn step(&mut self, _budget: usize) -> crate::models::traits::StepResult {
186            unimplemented!()
187        }
188        
189        fn is_complete(&self) -> bool {
190            self.complete
191        }
192        
193        fn get_telemetry(&self) -> Telemetry {
194            Telemetry {
195                total_comparisons: 0,
196                total_moves: 0,
197                memory_current: 0,
198                memory_peak: 0,
199                highlights: vec![],
200                markers: crate::models::traits::Markers::default(),
201                status_text: String::new(),
202                progress_hint: self.progress,
203            }
204        }
205        
206        fn reset(&mut self, _data: Vec<i32>) {}
207        
208        fn name(&self) -> &str {
209            &self.name
210        }
211        
212        fn get_array(&self) -> &[i32] {
213            &[]
214        }
215        
216        fn get_memory_usage(&self) -> usize {
217            0
218        }
219        
220        fn as_any(&self) -> &dyn std::any::Any {
221            self
222        }
223        
224        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
225            self
226        }
227    }
228    
229    #[test]
230    fn test_adaptive_fairness_creation() {
231        let model = AdaptiveFairness::new(0.2);
232        assert_eq!(model.learning_rate, 0.2);
233        assert_eq!(model.name(), "Adaptive Fairness");
234    }
235    
236    #[test]
237    fn test_adaptive_fairness_budget_allocation() {
238        let model = AdaptiveFairness::new(0.1);
239        let algorithms: Vec<Box<dyn Sorter>> = vec![
240            Box::new(MockSorter::new("Test1", 0.5)),
241            Box::new(MockSorter::new("Test2", 0.3)),
242        ];
243        
244        let budgets = model.allocate_budget(&algorithms);
245        
246        assert_eq!(budgets.len(), 2);
247        assert!(budgets[0] > 0);
248        assert!(budgets[1] > 0);
249    }
250}