fugue_evo/hyperparameter/
adaptive.rs1use rand::distributions::WeightedIndex;
17use rand::prelude::Distribution;
18use rand::Rng;
19use std::collections::VecDeque;
20
21#[derive(Clone, Debug)]
28pub struct OneFifthRule {
29 pub increase_factor: f64,
31 pub decrease_factor: f64,
33 pub window_size: usize,
35 pub target_success_rate: f64,
37 success_history: VecDeque<bool>,
39}
40
41impl OneFifthRule {
42 pub fn new() -> Self {
44 Self {
45 increase_factor: 1.22,
46 decrease_factor: 0.82,
47 window_size: 10,
48 target_success_rate: 0.2,
49 success_history: VecDeque::with_capacity(10),
50 }
51 }
52
53 pub fn with_factors(mut self, increase: f64, decrease: f64) -> Self {
55 self.increase_factor = increase;
56 self.decrease_factor = decrease;
57 self
58 }
59
60 pub fn with_window_size(mut self, size: usize) -> Self {
62 self.window_size = size;
63 self.success_history = VecDeque::with_capacity(size);
64 self
65 }
66
67 pub fn with_target_rate(mut self, rate: f64) -> Self {
69 self.target_success_rate = rate;
70 self
71 }
72
73 pub fn record(&mut self, success: bool) {
75 self.success_history.push_back(success);
76 if self.success_history.len() > self.window_size {
77 self.success_history.pop_front();
78 }
79 }
80
81 pub fn success_rate(&self) -> Option<f64> {
83 if self.success_history.is_empty() {
84 return None;
85 }
86 let successes = self.success_history.iter().filter(|&&s| s).count();
87 Some(successes as f64 / self.success_history.len() as f64)
88 }
89
90 pub fn adapt(&self, sigma: f64) -> f64 {
92 if self.success_history.len() < self.window_size {
93 return sigma;
94 }
95
96 let success_rate = self.success_rate().unwrap_or(self.target_success_rate);
97
98 if success_rate > self.target_success_rate {
99 sigma * self.increase_factor
100 } else if success_rate < self.target_success_rate {
101 sigma * self.decrease_factor
102 } else {
103 sigma
104 }
105 }
106
107 pub fn reset(&mut self) {
109 self.success_history.clear();
110 }
111}
112
113impl Default for OneFifthRule {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119#[derive(Clone, Debug)]
124pub struct AdaptiveOperatorSelection {
125 pub num_operators: usize,
127 pub weights: Vec<f64>,
129 pub learning_rate: f64,
131 pub min_probability: f64,
133 pub decay: f64,
135}
136
137impl AdaptiveOperatorSelection {
138 pub fn new(num_operators: usize) -> Self {
140 assert!(num_operators > 0, "Must have at least one operator");
141 Self {
142 num_operators,
143 weights: vec![1.0 / num_operators as f64; num_operators],
144 learning_rate: 0.1,
145 min_probability: 0.05,
146 decay: 0.99,
147 }
148 }
149
150 pub fn with_learning_rate(mut self, rate: f64) -> Self {
152 self.learning_rate = rate;
153 self
154 }
155
156 pub fn with_min_probability(mut self, prob: f64) -> Self {
158 self.min_probability = prob;
159 self
160 }
161
162 pub fn with_decay(mut self, decay: f64) -> Self {
164 self.decay = decay;
165 self
166 }
167
168 pub fn select<R: Rng>(&self, rng: &mut R) -> usize {
170 let dist = WeightedIndex::new(&self.weights).unwrap();
171 dist.sample(rng)
172 }
173
174 pub fn update(&mut self, operator_idx: usize, fitness_improvement: f64) {
176 assert!(operator_idx < self.num_operators);
177
178 for w in &mut self.weights {
180 *w *= self.decay;
181 }
182
183 let reward = fitness_improvement.max(0.0);
185 self.weights[operator_idx] += self.learning_rate * reward;
186
187 self.normalize_weights();
189 }
190
191 fn normalize_weights(&mut self) {
193 let sum: f64 = self.weights.iter().sum();
194 if sum <= 0.0 {
195 for w in &mut self.weights {
197 *w = 1.0 / self.num_operators as f64;
198 }
199 return;
200 }
201
202 for w in &mut self.weights {
204 *w /= sum;
205 }
206
207 let n = self.num_operators as f64;
209 let mut deficit = 0.0;
210 let mut excess_count = 0;
211
212 for w in &mut self.weights {
213 if *w < self.min_probability / n {
214 deficit += self.min_probability / n - *w;
215 *w = self.min_probability / n;
216 } else {
217 excess_count += 1;
218 }
219 }
220
221 if deficit > 0.0 && excess_count > 0 {
223 let reduction = deficit / excess_count as f64;
224 for w in &mut self.weights {
225 if *w > self.min_probability / n + reduction {
226 *w -= reduction;
227 }
228 }
229 }
230
231 let sum: f64 = self.weights.iter().sum();
233 for w in &mut self.weights {
234 *w /= sum;
235 }
236 }
237
238 pub fn probabilities(&self) -> &[f64] {
240 &self.weights
241 }
242
243 pub fn reset(&mut self) {
245 for w in &mut self.weights {
246 *w = 1.0 / self.num_operators as f64;
247 }
248 }
249}
250
251#[derive(Clone, Debug)]
253pub struct SlidingWindowStats {
254 values: VecDeque<f64>,
256 window_size: usize,
258}
259
260impl SlidingWindowStats {
261 pub fn new(window_size: usize) -> Self {
263 Self {
264 values: VecDeque::with_capacity(window_size),
265 window_size,
266 }
267 }
268
269 pub fn push(&mut self, value: f64) {
271 self.values.push_back(value);
272 if self.values.len() > self.window_size {
273 self.values.pop_front();
274 }
275 }
276
277 pub fn mean(&self) -> Option<f64> {
279 if self.values.is_empty() {
280 return None;
281 }
282 Some(self.values.iter().sum::<f64>() / self.values.len() as f64)
283 }
284
285 pub fn variance(&self) -> Option<f64> {
287 if self.values.len() < 2 {
288 return None;
289 }
290 let mean = self.mean()?;
291 let sum_sq: f64 = self.values.iter().map(|v| (v - mean).powi(2)).sum();
292 Some(sum_sq / (self.values.len() - 1) as f64)
293 }
294
295 pub fn std_dev(&self) -> Option<f64> {
297 self.variance().map(|v| v.sqrt())
298 }
299
300 pub fn min(&self) -> Option<f64> {
302 self.values.iter().copied().reduce(f64::min)
303 }
304
305 pub fn max(&self) -> Option<f64> {
307 self.values.iter().copied().reduce(f64::max)
308 }
309
310 pub fn is_full(&self) -> bool {
312 self.values.len() >= self.window_size
313 }
314
315 pub fn len(&self) -> usize {
317 self.values.len()
318 }
319
320 pub fn is_empty(&self) -> bool {
322 self.values.is_empty()
323 }
324
325 pub fn clear(&mut self) {
327 self.values.clear();
328 }
329}
330
331#[derive(Clone, Debug)]
335pub struct AdaptiveMutationRate {
336 pub rate: f64,
338 pub min_rate: f64,
340 pub max_rate: f64,
342 pub increase_factor: f64,
344 pub decrease_factor: f64,
346 stats: SlidingWindowStats,
348 improvement_threshold: f64,
350}
351
352impl AdaptiveMutationRate {
353 pub fn new(initial_rate: f64) -> Self {
355 Self {
356 rate: initial_rate,
357 min_rate: 0.001,
358 max_rate: 0.5,
359 increase_factor: 1.1,
360 decrease_factor: 0.9,
361 stats: SlidingWindowStats::new(20),
362 improvement_threshold: 0.3, }
364 }
365
366 pub fn record(&mut self, improved: bool) {
368 self.stats.push(if improved { 1.0 } else { 0.0 });
369 }
370
371 pub fn adapt(&mut self) {
373 if !self.stats.is_full() {
374 return;
375 }
376
377 let improvement_rate = self.stats.mean().unwrap_or(0.0);
378
379 if improvement_rate < self.improvement_threshold {
380 self.rate = (self.rate * self.increase_factor).min(self.max_rate);
382 } else if improvement_rate > self.improvement_threshold * 1.5 {
383 self.rate = (self.rate * self.decrease_factor).max(self.min_rate);
385 }
386 }
387
388 pub fn current_rate(&self) -> f64 {
390 self.rate
391 }
392}
393
394#[derive(Clone, Debug)]
396pub struct DiversityBasedAdaptation {
397 diversity_history: SlidingWindowStats,
399 pub target_diversity: f64,
401 pub tolerance: f64,
403}
404
405impl DiversityBasedAdaptation {
406 pub fn new(target_diversity: f64) -> Self {
408 Self {
409 diversity_history: SlidingWindowStats::new(10),
410 target_diversity,
411 tolerance: 0.1,
412 }
413 }
414
415 pub fn record_diversity(&mut self, diversity: f64) {
417 self.diversity_history.push(diversity);
418 }
419
420 pub fn mutation_multiplier(&self) -> f64 {
424 let Some(current_diversity) = self.diversity_history.mean() else {
425 return 1.0;
426 };
427
428 if current_diversity < self.target_diversity * (1.0 - self.tolerance) {
429 1.5
431 } else if current_diversity > self.target_diversity * (1.0 + self.tolerance) {
432 0.8
434 } else {
435 1.0
436 }
437 }
438
439 pub fn selection_pressure_multiplier(&self) -> f64 {
443 let Some(current_diversity) = self.diversity_history.mean() else {
444 return 1.0;
445 };
446
447 if current_diversity < self.target_diversity * (1.0 - self.tolerance) {
448 0.8
450 } else if current_diversity > self.target_diversity * (1.0 + self.tolerance) {
451 1.2
453 } else {
454 1.0
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_one_fifth_rule_increase() {
465 let mut rule = OneFifthRule::new().with_window_size(5);
466
467 for _ in 0..5 {
469 rule.record(true);
470 }
471
472 let sigma = 1.0;
473 let new_sigma = rule.adapt(sigma);
474 assert!(new_sigma > sigma);
475 }
476
477 #[test]
478 fn test_one_fifth_rule_decrease() {
479 let mut rule = OneFifthRule::new().with_window_size(5);
480
481 for _ in 0..5 {
483 rule.record(false);
484 }
485
486 let sigma = 1.0;
487 let new_sigma = rule.adapt(sigma);
488 assert!(new_sigma < sigma);
489 }
490
491 #[test]
492 fn test_one_fifth_rule_at_target() {
493 let mut rule = OneFifthRule::new()
494 .with_window_size(5)
495 .with_target_rate(0.2);
496
497 rule.record(true);
499 for _ in 0..4 {
500 rule.record(false);
501 }
502
503 let sigma = 1.0;
504 let new_sigma = rule.adapt(sigma);
505 assert!((new_sigma - sigma).abs() < 1e-10);
506 }
507
508 #[test]
509 fn test_adaptive_operator_selection() {
510 let mut aos = AdaptiveOperatorSelection::new(3);
511 let mut rng = rand::thread_rng();
512
513 assert_eq!(aos.probabilities().len(), 3);
515 for &p in aos.probabilities() {
516 assert!((p - 1.0 / 3.0).abs() < 1e-10);
517 }
518
519 aos.update(0, 10.0);
521
522 assert!(aos.probabilities()[0] > aos.probabilities()[1]);
524
525 let _ = aos.select(&mut rng);
527 }
528
529 #[test]
530 fn test_sliding_window_stats() {
531 let mut stats = SlidingWindowStats::new(5);
532
533 assert!(stats.mean().is_none());
534
535 stats.push(1.0);
536 stats.push(2.0);
537 stats.push(3.0);
538
539 assert!((stats.mean().unwrap() - 2.0).abs() < 1e-10);
540 assert!((stats.min().unwrap() - 1.0).abs() < 1e-10);
541 assert!((stats.max().unwrap() - 3.0).abs() < 1e-10);
542
543 stats.push(4.0);
545 stats.push(5.0);
546 assert!(stats.is_full());
547
548 stats.push(6.0);
550 assert_eq!(stats.len(), 5);
551 assert!((stats.min().unwrap() - 2.0).abs() < 1e-10);
552 }
553
554 #[test]
555 fn test_adaptive_mutation_rate() {
556 let mut amr = AdaptiveMutationRate::new(0.1);
557
558 for _ in 0..25 {
560 amr.record(false);
561 }
562 amr.adapt();
563
564 assert!(amr.current_rate() > 0.1);
566 }
567
568 #[test]
569 fn test_diversity_based_adaptation() {
570 let mut dba = DiversityBasedAdaptation::new(0.5);
571
572 for _ in 0..10 {
574 dba.record_diversity(0.2);
575 }
576
577 assert!(dba.mutation_multiplier() > 1.0);
579 assert!(dba.selection_pressure_multiplier() < 1.0);
581 }
582}