1use scirs2_core::random::rngs::StdRng;
14use scirs2_core::random::{Rng, SeedableRng};
15use sklears_core::error::{Result, SklearsError};
16use std::collections::HashMap;
17use std::fmt::Debug;
18
19#[derive(Debug, Clone)]
21pub enum AllocationStrategy {
22 Uniform,
24 UCB { exploration_factor: f64 },
26 ThompsonSampling,
28 SuccessiveHalving { reduction_factor: f64 },
30 Custom(fn(&[ResourceConfiguration]) -> Vec<f64>),
32}
33
34#[derive(Debug, Clone)]
36pub struct ResourceConfiguration {
37 pub id: String,
39 pub parameters: HashMap<String, f64>,
41 pub evaluations: usize,
43 pub scores: Vec<f64>,
45 pub mean_score: f64,
47 pub variance: f64,
49 pub confidence_width: f64,
51 pub resources_allocated: f64,
53 pub efficiency: f64,
55 pub is_active: bool,
57}
58
59impl ResourceConfiguration {
60 pub fn new(id: String, parameters: HashMap<String, f64>) -> Self {
62 Self {
63 id,
64 parameters,
65 evaluations: 0,
66 scores: Vec::new(),
67 mean_score: 0.0,
68 variance: 0.0,
69 confidence_width: f64::INFINITY,
70 resources_allocated: 0.0,
71 efficiency: 0.0,
72 is_active: true,
73 }
74 }
75
76 pub fn add_evaluation(&mut self, score: f64, resources_used: f64) {
78 self.scores.push(score);
79 self.evaluations += 1;
80 self.resources_allocated += resources_used;
81
82 self.update_statistics();
84 }
85
86 pub fn update_statistics(&mut self) {
88 if self.scores.is_empty() {
89 return;
90 }
91
92 self.mean_score = self.scores.iter().sum::<f64>() / self.scores.len() as f64;
94
95 if self.scores.len() > 1 {
97 let variance = self
98 .scores
99 .iter()
100 .map(|&x| (x - self.mean_score).powi(2))
101 .sum::<f64>()
102 / (self.scores.len() - 1) as f64;
103 self.variance = variance;
104
105 let std_error = (variance / self.scores.len() as f64).sqrt();
107 self.confidence_width = 1.96 * std_error;
108 }
109
110 if self.resources_allocated > 0.0 {
112 self.efficiency = self.mean_score / self.resources_allocated;
113 }
114 }
115
116 pub fn upper_confidence_bound(&self, exploration_factor: f64, total_evaluations: usize) -> f64 {
118 if self.evaluations == 0 {
119 return f64::INFINITY;
120 }
121
122 let confidence_bonus = exploration_factor
123 * (2.0 * (total_evaluations as f64).ln() / self.evaluations as f64).sqrt();
124
125 self.mean_score + confidence_bonus
126 }
127
128 pub fn lower_confidence_bound(&self, exploration_factor: f64, total_evaluations: usize) -> f64 {
130 if self.evaluations == 0 {
131 return f64::NEG_INFINITY;
132 }
133
134 let confidence_bonus = exploration_factor
135 * (2.0 * (total_evaluations as f64).ln() / self.evaluations as f64).sqrt();
136
137 self.mean_score - confidence_bonus
138 }
139
140 pub fn should_promote(&self, threshold_percentile: f64, all_scores: &[f64]) -> bool {
142 if all_scores.is_empty() || self.evaluations == 0 {
143 return true;
144 }
145
146 let mut sorted_scores = all_scores.to_vec();
147 sorted_scores.sort_by(|a, b| a.partial_cmp(b).unwrap());
148
149 let threshold_idx = (threshold_percentile * sorted_scores.len() as f64) as usize;
150 let threshold = sorted_scores
151 .get(threshold_idx)
152 .unwrap_or(&f64::NEG_INFINITY);
153
154 self.mean_score >= *threshold
155 }
156}
157
158pub struct AdaptiveResourceAllocator {
160 strategy: AllocationStrategy,
162 configurations: Vec<ResourceConfiguration>,
164 total_budget: f64,
166 resources_used: f64,
168 min_allocation: f64,
170 max_allocation: f64,
172 rng: StdRng,
174}
175
176impl AdaptiveResourceAllocator {
177 pub fn new(
178 strategy: AllocationStrategy,
179 total_budget: f64,
180 min_allocation: f64,
181 max_allocation: f64,
182 random_state: Option<u64>,
183 ) -> Self {
184 let rng = match random_state {
185 Some(seed) => StdRng::seed_from_u64(seed),
186 None => {
187 use scirs2_core::random::thread_rng;
188 StdRng::from_rng(&mut thread_rng())
189 }
190 };
191
192 Self {
193 strategy,
194 configurations: Vec::new(),
195 total_budget,
196 resources_used: 0.0,
197 min_allocation,
198 max_allocation,
199 rng,
200 }
201 }
202
203 pub fn add_configuration(&mut self, config: ResourceConfiguration) {
205 self.configurations.push(config);
206 }
207
208 pub fn allocate_resources(&mut self) -> Vec<(String, f64)> {
210 let active_configs: Vec<ResourceConfiguration> = self
211 .configurations
212 .iter()
213 .filter(|c| c.is_active)
214 .cloned()
215 .collect();
216
217 if active_configs.is_empty() {
218 return Vec::new();
219 }
220
221 let remaining_budget = self.total_budget - self.resources_used;
222 if remaining_budget <= 0.0 {
223 return Vec::new();
224 }
225
226 let active_refs: Vec<&ResourceConfiguration> = active_configs.iter().collect();
227 let allocations = match &self.strategy {
228 AllocationStrategy::Uniform => self.uniform_allocation(&active_refs, remaining_budget),
229 AllocationStrategy::UCB { exploration_factor } => {
230 self.ucb_allocation(&active_refs, remaining_budget, *exploration_factor)
231 }
232 AllocationStrategy::ThompsonSampling => {
233 self.thompson_sampling_allocation(&active_configs, remaining_budget)
234 }
235 AllocationStrategy::SuccessiveHalving { reduction_factor } => self
236 .successive_halving_allocation(&active_refs, remaining_budget, *reduction_factor),
237 AllocationStrategy::Custom(allocation_fn) => {
238 self.custom_allocation(&active_refs, remaining_budget, allocation_fn)
239 }
240 };
241
242 let constrained_allocations: Vec<(String, f64)> = allocations
244 .into_iter()
245 .map(|(id, alloc)| (id, alloc.clamp(self.min_allocation, self.max_allocation)))
246 .collect();
247
248 let total_allocated: f64 = constrained_allocations.iter().map(|(_, alloc)| alloc).sum();
250 self.resources_used += total_allocated;
251
252 constrained_allocations
253 }
254
255 fn uniform_allocation(
257 &self,
258 configs: &[&ResourceConfiguration],
259 budget: f64,
260 ) -> Vec<(String, f64)> {
261 let allocation_per_config = budget / configs.len() as f64;
262 configs
263 .iter()
264 .map(|config| (config.id.clone(), allocation_per_config))
265 .collect()
266 }
267
268 fn ucb_allocation(
270 &self,
271 configs: &[&ResourceConfiguration],
272 budget: f64,
273 exploration_factor: f64,
274 ) -> Vec<(String, f64)> {
275 let total_evaluations: usize = configs.iter().map(|c| c.evaluations).sum();
276
277 let ucb_scores: Vec<f64> = configs
279 .iter()
280 .map(|config| config.upper_confidence_bound(exploration_factor, total_evaluations))
281 .collect();
282
283 let total_ucb: f64 = ucb_scores.iter().sum();
285 if total_ucb <= 0.0 {
286 return self.uniform_allocation(configs, budget);
287 }
288
289 configs
290 .iter()
291 .zip(ucb_scores.iter())
292 .map(|(config, &ucb)| {
293 let allocation = budget * (ucb / total_ucb);
294 (config.id.clone(), allocation)
295 })
296 .collect()
297 }
298
299 fn thompson_sampling_allocation(
301 &mut self,
302 configs: &[ResourceConfiguration],
303 budget: f64,
304 ) -> Vec<(String, f64)> {
305 let mut samples = Vec::new();
306
307 for config in configs {
308 let sample = if config.evaluations == 0 {
309 self.rng.gen::<f64>()
311 } else {
312 let std_dev = if config.variance > 0.0 {
314 (config.variance / config.evaluations as f64).sqrt()
315 } else {
316 0.1 };
318
319 use scirs2_core::random::essentials::Normal;
320 let normal = Normal::new(config.mean_score, std_dev)
321 .unwrap_or_else(|_| Normal::new(0.0, 1.0).unwrap());
322 self.rng.sample(normal)
323 };
324 samples.push(sample);
325 }
326
327 let total_samples: f64 = samples.iter().sum();
329 if total_samples <= 0.0 {
330 let config_refs: Vec<&ResourceConfiguration> = configs.iter().collect();
331 return self.uniform_allocation(&config_refs, budget);
332 }
333
334 configs
335 .iter()
336 .zip(samples.iter())
337 .map(|(config, &sample)| {
338 let allocation = budget * (sample / total_samples);
339 (config.id.clone(), allocation)
340 })
341 .collect()
342 }
343
344 fn successive_halving_allocation(
346 &self,
347 configs: &[&ResourceConfiguration],
348 budget: f64,
349 reduction_factor: f64,
350 ) -> Vec<(String, f64)> {
351 let mut config_scores: Vec<(usize, f64)> = configs
353 .iter()
354 .enumerate()
355 .map(|(i, config)| (i, config.mean_score))
356 .collect();
357
358 config_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
359
360 let num_to_keep = ((configs.len() as f64) * reduction_factor).ceil() as usize;
362 let num_to_keep = num_to_keep.max(1);
363
364 let mut allocations = vec![(String::new(), 0.0); configs.len()];
365 let allocation_per_kept = budget / num_to_keep as f64;
366
367 for (config_idx, _score) in config_scores
368 .iter()
369 .take(num_to_keep.min(config_scores.len()))
370 {
371 allocations[*config_idx] = (configs[*config_idx].id.clone(), allocation_per_kept);
372 }
373
374 allocations
375 .into_iter()
376 .filter(|(id, _)| !id.is_empty())
377 .collect()
378 }
379
380 fn custom_allocation(
382 &self,
383 configs: &[&ResourceConfiguration],
384 budget: f64,
385 allocation_fn: &fn(&[ResourceConfiguration]) -> Vec<f64>,
386 ) -> Vec<(String, f64)> {
387 let config_refs: Vec<ResourceConfiguration> = configs.iter().map(|&c| c.clone()).collect();
388 let allocations = allocation_fn(&config_refs);
389
390 if allocations.len() != configs.len() {
391 return self.uniform_allocation(configs, budget);
392 }
393
394 let total_weight: f64 = allocations.iter().sum();
395 if total_weight <= 0.0 {
396 return self.uniform_allocation(configs, budget);
397 }
398
399 configs
400 .iter()
401 .zip(allocations.iter())
402 .map(|(config, &weight)| {
403 let allocation = budget * (weight / total_weight);
404 (config.id.clone(), allocation)
405 })
406 .collect()
407 }
408
409 pub fn update_configuration(
411 &mut self,
412 config_id: &str,
413 score: f64,
414 resources_used: f64,
415 ) -> Result<()> {
416 let config = self
417 .configurations
418 .iter_mut()
419 .find(|c| c.id == config_id)
420 .ok_or_else(|| {
421 SklearsError::InvalidInput(format!("Configuration {} not found", config_id))
422 })?;
423
424 config.add_evaluation(score, resources_used);
425 Ok(())
426 }
427
428 pub fn prune_configurations(&mut self, min_evaluations: usize, percentile_threshold: f64) {
430 if self.configurations.len() <= 1 {
431 return;
432 }
433
434 let eligible_scores: Vec<f64> = self
436 .configurations
437 .iter()
438 .filter(|c| c.evaluations >= min_evaluations)
439 .map(|c| c.mean_score)
440 .collect();
441
442 if eligible_scores.is_empty() {
443 return;
444 }
445
446 for config in &mut self.configurations {
447 if config.evaluations >= min_evaluations
448 && !config.should_promote(percentile_threshold, &eligible_scores)
449 {
450 config.is_active = false;
451 }
452 }
453 }
454
455 pub fn get_statistics(&self) -> AllocationStatistics {
457 let active_count = self.configurations.iter().filter(|c| c.is_active).count();
458 let total_count = self.configurations.len();
459
460 let best_score = self
461 .configurations
462 .iter()
463 .filter(|c| c.evaluations > 0)
464 .map(|c| c.mean_score)
465 .fold(f64::NEG_INFINITY, |a, b| a.max(b));
466
467 let total_evaluations: usize = self.configurations.iter().map(|c| c.evaluations).sum();
468
469 AllocationStatistics {
470 active_configurations: active_count,
471 total_configurations: total_count,
472 resources_used: self.resources_used,
473 resources_remaining: self.total_budget - self.resources_used,
474 best_score,
475 total_evaluations,
476 }
477 }
478
479 pub fn active_configurations(&self) -> Vec<&ResourceConfiguration> {
481 self.configurations.iter().filter(|c| c.is_active).collect()
482 }
483
484 pub fn best_configuration(&self) -> Option<&ResourceConfiguration> {
486 self.configurations
487 .iter()
488 .filter(|c| c.evaluations > 0)
489 .max_by(|a, b| a.mean_score.partial_cmp(&b.mean_score).unwrap())
490 }
491}
492
493#[derive(Debug, Clone)]
495pub struct AllocationStatistics {
496 pub active_configurations: usize,
497 pub total_configurations: usize,
498 pub resources_used: f64,
499 pub resources_remaining: f64,
500 pub best_score: f64,
501 pub total_evaluations: usize,
502}
503
504#[derive(Debug, Clone)]
506pub struct AdaptiveAllocationConfig {
507 pub total_budget: f64,
509 pub min_allocation: f64,
511 pub max_allocation: f64,
513 pub min_evaluations_for_pruning: usize,
515 pub pruning_percentile: f64,
517 pub pruning_frequency: usize,
519 pub random_state: Option<u64>,
521}
522
523impl Default for AdaptiveAllocationConfig {
524 fn default() -> Self {
525 Self {
526 total_budget: 1000.0,
527 min_allocation: 1.0,
528 max_allocation: 100.0,
529 min_evaluations_for_pruning: 3,
530 pruning_percentile: 0.25,
531 pruning_frequency: 5,
532 random_state: None,
533 }
534 }
535}
536
537#[allow(non_snake_case)]
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
543 fn test_resource_configuration() {
544 let mut config = ResourceConfiguration::new("test".to_string(), HashMap::new());
545 assert_eq!(config.evaluations, 0);
546 assert!(config.is_active);
547
548 config.add_evaluation(0.8, 10.0);
549 assert_eq!(config.evaluations, 1);
550 assert_eq!(config.mean_score, 0.8);
551 assert_eq!(config.resources_allocated, 10.0);
552 }
553
554 #[test]
555 fn test_ucb_calculation() {
556 let mut config = ResourceConfiguration::new("test".to_string(), HashMap::new());
557 config.add_evaluation(0.8, 10.0);
558
559 let ucb = config.upper_confidence_bound(1.0, 10);
560 assert!(ucb > config.mean_score);
561 }
562
563 #[test]
564 fn test_uniform_allocation() {
565 let allocator =
566 AdaptiveResourceAllocator::new(AllocationStrategy::Uniform, 100.0, 1.0, 50.0, Some(42));
567
568 let configs = vec![
569 ResourceConfiguration::new("1".to_string(), HashMap::new()),
570 ResourceConfiguration::new("2".to_string(), HashMap::new()),
571 ];
572 let config_refs: Vec<&ResourceConfiguration> = configs.iter().collect();
573
574 let allocations = allocator.uniform_allocation(&config_refs, 100.0);
575 assert_eq!(allocations.len(), 2);
576 assert_eq!(allocations[0].1, 50.0);
577 assert_eq!(allocations[1].1, 50.0);
578 }
579
580 #[test]
581 fn test_configuration_pruning() {
582 let mut config = ResourceConfiguration::new("test".to_string(), HashMap::new());
583 config.add_evaluation(0.1, 10.0); let all_scores = vec![0.8, 0.9, 0.7, 0.1]; assert!(!config.should_promote(0.5, &all_scores)); }
588
589 #[test]
590 fn test_allocation_statistics() {
591 let mut allocator =
592 AdaptiveResourceAllocator::new(AllocationStrategy::Uniform, 100.0, 1.0, 50.0, Some(42));
593
594 allocator.add_configuration(ResourceConfiguration::new("1".to_string(), HashMap::new()));
595 allocator.add_configuration(ResourceConfiguration::new("2".to_string(), HashMap::new()));
596
597 let stats = allocator.get_statistics();
598 assert_eq!(stats.active_configurations, 2);
599 assert_eq!(stats.total_configurations, 2);
600 assert_eq!(stats.resources_used, 0.0);
601 }
602}