rustyqlib/core/optimization/
differential_evolution.rs1use super::{OptimConfig, OptimResult};
6
7const DIFFERENTIAL_WEIGHT: f64 = 0.8; const CROSSOVER_RATE: f64 = 0.9; pub fn differential_evolution(
15 cfg: &OptimConfig,
16 f: &dyn Fn(&[f64]) -> f64,
17 bounds: &[(f64, f64)],
18 seed: u64,
19) -> OptimResult {
20 let dim = bounds.len();
21 assert!(dim > 0, "bounds must give at least one parameter");
22 for &(lo, hi) in bounds {
23 assert!(lo < hi, "each bound needs lo < hi");
24 }
25 let np = (10 * dim).max(15);
26 let mut rng = Xorshift64Star::new(seed);
27
28 let mut pop: Vec<Vec<f64>> = (0..np)
30 .map(|_| bounds.iter().map(|&(lo, hi)| lo + (hi - lo) * rng.uniform()).collect())
31 .collect();
32 let mut values: Vec<f64> = pop.iter().map(|x| f(x)).collect();
33
34 for gen in 1..=cfg.max_iter {
35 for i in 0..np {
36 let mut pick = || loop {
38 let j = (rng.next() % np as u64) as usize;
39 if j != i {
40 return j;
41 }
42 };
43 let (a, b, c) = {
44 let a = pick();
45 let b = loop {
46 let b = pick();
47 if b != a {
48 break b;
49 }
50 };
51 let c = loop {
52 let c = pick();
53 if c != a && c != b {
54 break c;
55 }
56 };
57 (a, b, c)
58 };
59 let j_rand = (rng.next() % dim as u64) as usize;
61 let mut trial = pop[i].clone();
62 for j in 0..dim {
63 if j == j_rand || rng.uniform() < CROSSOVER_RATE {
64 let v = pop[a][j] + DIFFERENTIAL_WEIGHT * (pop[b][j] - pop[c][j]);
65 trial[j] = v.clamp(bounds[j].0, bounds[j].1);
66 }
67 }
68 let f_trial = f(&trial);
69 if f_trial <= values[i] {
70 pop[i] = trial;
71 values[i] = f_trial;
72 }
73 }
74 let best = values.iter().cloned().fold(f64::INFINITY, f64::min);
75 let worst = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
76 if worst - best <= cfg.tol * (1.0 + best.abs()) {
77 let (i, &value) =
78 values.iter().enumerate().min_by(|a, b| a.1.total_cmp(b.1)).expect("non-empty");
79 return OptimResult { x: pop[i].clone(), value, iterations: gen, converged: true };
80 }
81 }
82 let (i, &value) =
83 values.iter().enumerate().min_by(|a, b| a.1.total_cmp(b.1)).expect("non-empty");
84 OptimResult { x: pop[i].clone(), value, iterations: cfg.max_iter, converged: false }
85}
86
87struct Xorshift64Star {
90 state: u64,
91}
92
93impl Xorshift64Star {
94 fn new(seed: u64) -> Self {
95 Self { state: seed.max(1) }
96 }
97 fn next(&mut self) -> u64 {
98 let mut x = self.state;
99 x ^= x >> 12;
100 x ^= x << 25;
101 x ^= x >> 27;
102 self.state = x;
103 x.wrapping_mul(0x2545F4914F6CDD1D)
104 }
105 fn uniform(&mut self) -> f64 {
106 (self.next() >> 11) as f64 / (1u64 << 53) as f64
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::f64::consts::PI;
114
115 #[test]
116 fn finds_the_global_minimum_of_rastrigin() {
117 let f = |x: &[f64]| {
119 20.0 + x.iter().map(|xi| xi * xi - 10.0 * (2.0 * PI * xi).cos()).sum::<f64>()
120 };
121 let bounds = [(-5.12, 5.12), (-5.12, 5.12)];
122 let r = differential_evolution(&OptimConfig::new(1e-10, 600), &f, &bounds, 7);
123 assert!(r.value < 1e-6, "stuck at a local minimum: {r:?}");
124 assert!(r.x.iter().all(|xi| xi.abs() < 1e-3), "{:?}", r.x);
125 }
126
127 #[test]
128 fn is_deterministic_for_a_seed_and_respects_bounds() {
129 let f = |x: &[f64]| (x[0] - 0.5).powi(2) + (x[1] - 0.25).powi(2);
130 let bounds = [(0.0, 1.0), (0.0, 1.0)];
131 let cfg = OptimConfig::new(1e-12, 300);
132 let a = differential_evolution(&cfg, &f, &bounds, 123);
133 let b = differential_evolution(&cfg, &f, &bounds, 123);
134 assert_eq!(a.x, b.x, "same seed must reproduce the same run");
135 assert!(a.x.iter().all(|&v| (0.0..=1.0).contains(&v)));
136 assert!((a.x[0] - 0.5).abs() < 1e-5 && (a.x[1] - 0.25).abs() < 1e-5, "{a:?}");
137 let c = differential_evolution(&cfg, &f, &bounds, 999);
139 assert!((c.x[0] - 0.5).abs() < 1e-5, "{c:?}");
140 }
141}