fugue_evo/hyperparameter/
adaptive.rs1use rand::distributions::WeightedIndex;
6use rand::prelude::Distribution;
7use rand::Rng;
8use std::collections::VecDeque;
9
10#[derive(Clone, Debug)]
17pub struct OneFifthRule {
18 pub increase_factor: f64,
20 pub decrease_factor: f64,
22 pub window_size: usize,
24 pub target_success_rate: f64,
26 success_history: VecDeque<bool>,
28}
29
30impl OneFifthRule {
31 pub fn new() -> Self {
33 Self {
34 increase_factor: 1.22,
35 decrease_factor: 0.82,
36 window_size: 10,
37 target_success_rate: 0.2,
38 success_history: VecDeque::with_capacity(10),
39 }
40 }
41
42 pub fn with_factors(mut self, increase: f64, decrease: f64) -> Self {
44 self.increase_factor = increase;
45 self.decrease_factor = decrease;
46 self
47 }
48
49 pub fn with_window_size(mut self, size: usize) -> Self {
51 self.window_size = size;
52 self.success_history = VecDeque::with_capacity(size);
53 self
54 }
55
56 pub fn with_target_rate(mut self, rate: f64) -> Self {
58 self.target_success_rate = rate;
59 self
60 }
61
62 pub fn record(&mut self, success: bool) {
64 self.success_history.push_back(success);
65 if self.success_history.len() > self.window_size {
66 self.success_history.pop_front();
67 }
68 }
69
70 pub fn success_rate(&self) -> Option<f64> {
72 if self.success_history.is_empty() {
73 return None;
74 }
75 let successes = self.success_history.iter().filter(|&&s| s).count();
76 Some(successes as f64 / self.success_history.len() as f64)
77 }
78
79 pub fn adapt(&self, sigma: f64) -> f64 {
81 if self.success_history.len() < self.window_size {
82 return sigma;
83 }
84
85 let success_rate = self.success_rate().unwrap_or(self.target_success_rate);
86
87 if success_rate > self.target_success_rate {
88 sigma * self.increase_factor
89 } else if success_rate < self.target_success_rate {
90 sigma * self.decrease_factor
91 } else {
92 sigma
93 }
94 }
95
96 pub fn reset(&mut self) {
98 self.success_history.clear();
99 }
100}
101
102impl Default for OneFifthRule {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108#[derive(Clone, Debug)]
113pub struct AdaptiveOperatorSelection {
114 pub num_operators: usize,
116 pub weights: Vec<f64>,
118 pub learning_rate: f64,
120 pub min_probability: f64,
122 pub decay: f64,
124}
125
126impl AdaptiveOperatorSelection {
127 pub fn new(num_operators: usize) -> Self {
129 assert!(num_operators > 0, "Must have at least one operator");
130 Self {
131 num_operators,
132 weights: vec![1.0 / num_operators as f64; num_operators],
133 learning_rate: 0.1,
134 min_probability: 0.05,
135 decay: 0.99,
136 }
137 }
138
139 pub fn with_learning_rate(mut self, rate: f64) -> Self {
141 self.learning_rate = rate;
142 self
143 }
144
145 pub fn with_min_probability(mut self, prob: f64) -> Self {
147 self.min_probability = prob;
148 self
149 }
150
151 pub fn with_decay(mut self, decay: f64) -> Self {
153 self.decay = decay;
154 self
155 }
156
157 pub fn select<R: Rng>(&self, rng: &mut R) -> usize {
159 let dist = WeightedIndex::new(&self.weights).unwrap();
160 dist.sample(rng)
161 }
162
163 pub fn update(&mut self, operator_idx: usize, fitness_improvement: f64) {
165 assert!(operator_idx < self.num_operators);
166
167 for w in &mut self.weights {
169 *w *= self.decay;
170 }
171
172 let reward = fitness_improvement.max(0.0);
174 self.weights[operator_idx] += self.learning_rate * reward;
175
176 self.normalize_weights();
178 }
179
180 fn normalize_weights(&mut self) {
182 let sum: f64 = self.weights.iter().sum();
183 if sum <= 0.0 {
184 for w in &mut self.weights {
186 *w = 1.0 / self.num_operators as f64;
187 }
188 return;
189 }
190
191 for w in &mut self.weights {
193 *w /= sum;
194 }
195
196 let n = self.num_operators as f64;
198 let mut deficit = 0.0;
199 let mut excess_count = 0;
200
201 for w in &mut self.weights {
202 if *w < self.min_probability / n {
203 deficit += self.min_probability / n - *w;
204 *w = self.min_probability / n;
205 } else {
206 excess_count += 1;
207 }
208 }
209
210 if deficit > 0.0 && excess_count > 0 {
212 let reduction = deficit / excess_count as f64;
213 for w in &mut self.weights {
214 if *w > self.min_probability / n + reduction {
215 *w -= reduction;
216 }
217 }
218 }
219
220 let sum: f64 = self.weights.iter().sum();
222 for w in &mut self.weights {
223 *w /= sum;
224 }
225 }
226
227 pub fn probabilities(&self) -> &[f64] {
229 &self.weights
230 }
231
232 pub fn reset(&mut self) {
234 for w in &mut self.weights {
235 *w = 1.0 / self.num_operators as f64;
236 }
237 }
238}
239
240#[derive(Clone, Debug)]
242pub struct SlidingWindowStats {
243 values: VecDeque<f64>,
245 window_size: usize,
247}
248
249impl SlidingWindowStats {
250 pub fn new(window_size: usize) -> Self {
252 Self {
253 values: VecDeque::with_capacity(window_size),
254 window_size,
255 }
256 }
257
258 pub fn push(&mut self, value: f64) {
260 self.values.push_back(value);
261 if self.values.len() > self.window_size {
262 self.values.pop_front();
263 }
264 }
265
266 pub fn mean(&self) -> Option<f64> {
268 if self.values.is_empty() {
269 return None;
270 }
271 Some(self.values.iter().sum::<f64>() / self.values.len() as f64)
272 }
273
274 pub fn variance(&self) -> Option<f64> {
276 if self.values.len() < 2 {
277 return None;
278 }
279 let mean = self.mean()?;
280 let sum_sq: f64 = self.values.iter().map(|v| (v - mean).powi(2)).sum();
281 Some(sum_sq / (self.values.len() - 1) as f64)
282 }
283
284 pub fn std_dev(&self) -> Option<f64> {
286 self.variance().map(|v| v.sqrt())
287 }
288
289 pub fn min(&self) -> Option<f64> {
291 self.values.iter().copied().reduce(f64::min)
292 }
293
294 pub fn max(&self) -> Option<f64> {
296 self.values.iter().copied().reduce(f64::max)
297 }
298
299 pub fn is_full(&self) -> bool {
301 self.values.len() >= self.window_size
302 }
303
304 pub fn len(&self) -> usize {
306 self.values.len()
307 }
308
309 pub fn is_empty(&self) -> bool {
311 self.values.is_empty()
312 }
313
314 pub fn clear(&mut self) {
316 self.values.clear();
317 }
318}
319
320#[derive(Clone, Debug)]
324pub struct AdaptiveMutationRate {
325 pub rate: f64,
327 pub min_rate: f64,
329 pub max_rate: f64,
331 pub increase_factor: f64,
333 pub decrease_factor: f64,
335 stats: SlidingWindowStats,
337 improvement_threshold: f64,
339}
340
341impl AdaptiveMutationRate {
342 pub fn new(initial_rate: f64) -> Self {
344 Self {
345 rate: initial_rate,
346 min_rate: 0.001,
347 max_rate: 0.5,
348 increase_factor: 1.1,
349 decrease_factor: 0.9,
350 stats: SlidingWindowStats::new(20),
351 improvement_threshold: 0.3, }
353 }
354
355 pub fn record(&mut self, improved: bool) {
357 self.stats.push(if improved { 1.0 } else { 0.0 });
358 }
359
360 pub fn adapt(&mut self) {
362 if !self.stats.is_full() {
363 return;
364 }
365
366 let improvement_rate = self.stats.mean().unwrap_or(0.0);
367
368 if improvement_rate < self.improvement_threshold {
369 self.rate = (self.rate * self.increase_factor).min(self.max_rate);
371 } else if improvement_rate > self.improvement_threshold * 1.5 {
372 self.rate = (self.rate * self.decrease_factor).max(self.min_rate);
374 }
375 }
376
377 pub fn current_rate(&self) -> f64 {
379 self.rate
380 }
381}
382
383#[derive(Clone, Debug)]
385pub struct DiversityBasedAdaptation {
386 diversity_history: SlidingWindowStats,
388 pub target_diversity: f64,
390 pub tolerance: f64,
392}
393
394impl DiversityBasedAdaptation {
395 pub fn new(target_diversity: f64) -> Self {
397 Self {
398 diversity_history: SlidingWindowStats::new(10),
399 target_diversity,
400 tolerance: 0.1,
401 }
402 }
403
404 pub fn record_diversity(&mut self, diversity: f64) {
406 self.diversity_history.push(diversity);
407 }
408
409 pub fn mutation_multiplier(&self) -> f64 {
413 let Some(current_diversity) = self.diversity_history.mean() else {
414 return 1.0;
415 };
416
417 if current_diversity < self.target_diversity * (1.0 - self.tolerance) {
418 1.5
420 } else if current_diversity > self.target_diversity * (1.0 + self.tolerance) {
421 0.8
423 } else {
424 1.0
425 }
426 }
427
428 pub fn selection_pressure_multiplier(&self) -> f64 {
432 let Some(current_diversity) = self.diversity_history.mean() else {
433 return 1.0;
434 };
435
436 if current_diversity < self.target_diversity * (1.0 - self.tolerance) {
437 0.8
439 } else if current_diversity > self.target_diversity * (1.0 + self.tolerance) {
440 1.2
442 } else {
443 1.0
444 }
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_one_fifth_rule_increase() {
454 let mut rule = OneFifthRule::new().with_window_size(5);
455
456 for _ in 0..5 {
458 rule.record(true);
459 }
460
461 let sigma = 1.0;
462 let new_sigma = rule.adapt(sigma);
463 assert!(new_sigma > sigma);
464 }
465
466 #[test]
467 fn test_one_fifth_rule_decrease() {
468 let mut rule = OneFifthRule::new().with_window_size(5);
469
470 for _ in 0..5 {
472 rule.record(false);
473 }
474
475 let sigma = 1.0;
476 let new_sigma = rule.adapt(sigma);
477 assert!(new_sigma < sigma);
478 }
479
480 #[test]
481 fn test_one_fifth_rule_at_target() {
482 let mut rule = OneFifthRule::new()
483 .with_window_size(5)
484 .with_target_rate(0.2);
485
486 rule.record(true);
488 for _ in 0..4 {
489 rule.record(false);
490 }
491
492 let sigma = 1.0;
493 let new_sigma = rule.adapt(sigma);
494 assert!((new_sigma - sigma).abs() < 1e-10);
495 }
496
497 #[test]
498 fn test_adaptive_operator_selection() {
499 let mut aos = AdaptiveOperatorSelection::new(3);
500 let mut rng = rand::thread_rng();
501
502 assert_eq!(aos.probabilities().len(), 3);
504 for &p in aos.probabilities() {
505 assert!((p - 1.0 / 3.0).abs() < 1e-10);
506 }
507
508 aos.update(0, 10.0);
510
511 assert!(aos.probabilities()[0] > aos.probabilities()[1]);
513
514 let _ = aos.select(&mut rng);
516 }
517
518 #[test]
519 fn test_sliding_window_stats() {
520 let mut stats = SlidingWindowStats::new(5);
521
522 assert!(stats.mean().is_none());
523
524 stats.push(1.0);
525 stats.push(2.0);
526 stats.push(3.0);
527
528 assert!((stats.mean().unwrap() - 2.0).abs() < 1e-10);
529 assert!((stats.min().unwrap() - 1.0).abs() < 1e-10);
530 assert!((stats.max().unwrap() - 3.0).abs() < 1e-10);
531
532 stats.push(4.0);
534 stats.push(5.0);
535 assert!(stats.is_full());
536
537 stats.push(6.0);
539 assert_eq!(stats.len(), 5);
540 assert!((stats.min().unwrap() - 2.0).abs() < 1e-10);
541 }
542
543 #[test]
544 fn test_adaptive_mutation_rate() {
545 let mut amr = AdaptiveMutationRate::new(0.1);
546
547 for _ in 0..25 {
549 amr.record(false);
550 }
551 amr.adapt();
552
553 assert!(amr.current_rate() > 0.1);
555 }
556
557 #[test]
558 fn test_diversity_based_adaptation() {
559 let mut dba = DiversityBasedAdaptation::new(0.5);
560
561 for _ in 0..10 {
563 dba.record_diversity(0.2);
564 }
565
566 assert!(dba.mutation_multiplier() > 1.0);
568 assert!(dba.selection_pressure_multiplier() < 1.0);
570 }
571}