Skip to main content

fcmaes_core/
de.rs

1//! Differential Evolution — Rust port of the C++ `deoptimizer.cpp`.
2//!
3//! DE/best/1 with the fcmaes extensions: temporal locality (an extra
4//! improvement trial along the previous move), age-based reinitialization of
5//! stale individuals, oscillating `F`/`CR` between generations, optional
6//! normal-distributed sampling around a guess, and mixed-integer "modify"
7//! resampling. Replaces both the C++ optimizer and the pure-Python
8//! `fcmaes/de.py`. Cross-implementation parity is statistical.
9
10use std::collections::VecDeque;
11
12use crate::fitness::{Fitness, Objective};
13use crate::rng::Rng;
14
15/// Outcome of a DE run (mirrors the C++ `DeResult`).
16#[derive(Clone, Debug)]
17pub struct DeResult {
18    pub x: Vec<f64>,
19    pub y: f64,
20    pub evaluations: u64,
21    pub iterations: i32,
22    pub stop: i32,
23}
24
25/// Tunable inputs for [`De::new`]. Non-positive values select DE defaults.
26#[derive(Clone, Debug)]
27pub struct DeParams {
28    pub popsize: i32,
29    pub max_evaluations: u64,
30    pub keep: f64,
31    pub stop_fitness: f64,
32    pub f: f64,
33    pub cr: f64,
34    pub min_mutate: f64,
35    pub max_mutate: f64,
36    pub min_sigma: f64,
37    pub seed: u64,
38    pub runid: i64,
39}
40
41impl Default for DeParams {
42    fn default() -> Self {
43        Self {
44            popsize: 31,
45            max_evaluations: 100_000,
46            keep: 200.0,
47            stop_fitness: f64::NEG_INFINITY,
48            f: 0.5,
49            cr: 0.9,
50            min_mutate: 0.1,
51            max_mutate: 0.5,
52            min_sigma: 0.0,
53            seed: 0,
54            runid: 0,
55        }
56    }
57}
58
59pub struct De {
60    fitfun: Fitness,
61    rng: Rng,
62    dim: usize,
63    popsize: usize,
64    max_evaluations: u64,
65    keep: f64,
66    stopfitness: f64,
67    f0: f64,
68    cr0: f64,
69    f: f64,
70    cr: f64,
71    min_mutate: f64,
72    max_mutate: f64,
73    is_int: Option<Vec<bool>>,
74
75    // normal-sampling around a guess
76    use_normal: bool,
77    mean: Vec<f64>,
78    sigma: Vec<f64>,
79    max_sigma: Vec<f64>,
80    min_sigma_vec: Vec<f64>,
81    min_sigma_val: f64,
82    mean_hist: Vec<Vec<f64>>, // 10 columns, each dim long
83    mean_hist_index: usize,
84
85    // population
86    pop_x: Vec<Vec<f64>>,
87    pop_x0: Vec<Vec<f64>>,
88    pop_y: Vec<f64>,
89    pop_iter: Vec<i32>,
90    best_i: usize,
91    best_x: Vec<f64>,
92    best_y: f64,
93
94    iterations: i32,
95    stop: i32,
96    pos: usize,
97
98    // ask/tell bookkeeping
99    improves_x: VecDeque<Vec<f64>>,
100    improves_p: VecDeque<usize>,
101    asked_x: Vec<Vec<f64>>,
102    asked_p: Vec<usize>,
103    external_evaluations: u64,
104}
105
106impl De {
107    /// Build a DE optimizer. `guess`/`sigma` empty ⇒ uniform sampling in the
108    /// box; non-empty ⇒ normal sampling around `guess`. `ints` marks discrete
109    /// coordinates (length `dim`) or is `None`.
110    pub fn new(
111        mut fitfun: Fitness,
112        guess: &[f64],
113        sigma: &[f64],
114        ints: Option<Vec<bool>>,
115        p: &DeParams,
116    ) -> Self {
117        let dim = fitfun.dim();
118        fitfun.reset_evaluations();
119        let popsize = if p.popsize > 0 {
120            p.popsize as usize
121        } else {
122            15 * dim
123        };
124        let keep = if p.keep > 0.0 { p.keep } else { 30.0 };
125        let f0 = if p.f > 0.0 { p.f } else { 0.5 };
126        let cr0 = if p.cr > 0.0 { p.cr } else { 0.9 };
127        let min_mutate = if p.min_mutate > 0.0 {
128            p.min_mutate
129        } else {
130            0.1
131        };
132        let max_mutate = if p.max_mutate > 0.0 {
133            p.max_mutate
134        } else {
135            0.5
136        };
137
138        let use_normal = !guess.is_empty();
139        let mean = guess.to_vec();
140        let sigma_v = sigma.to_vec();
141
142        let mut de = De {
143            dim,
144            popsize,
145            max_evaluations: if p.max_evaluations > 0 {
146                p.max_evaluations
147            } else {
148                50_000
149            },
150            keep,
151            stopfitness: p.stop_fitness,
152            f0,
153            cr0,
154            f: f0,
155            cr: cr0,
156            min_mutate,
157            max_mutate,
158            is_int: ints,
159            use_normal,
160            mean,
161            sigma: sigma_v,
162            max_sigma: vec![],
163            min_sigma_vec: vec![],
164            min_sigma_val: p.min_sigma,
165            mean_hist: vec![],
166            mean_hist_index: 0,
167            pop_x: vec![],
168            pop_x0: vec![],
169            pop_y: vec![],
170            pop_iter: vec![],
171            best_i: 0,
172            best_x: vec![],
173            best_y: f64::MAX,
174            iterations: 0,
175            stop: 0,
176            pos: 0,
177            improves_x: VecDeque::new(),
178            improves_p: VecDeque::new(),
179            asked_x: vec![],
180            asked_p: vec![],
181            external_evaluations: 0,
182            rng: Rng::new(p.seed.wrapping_add(p.runid as u64)),
183            fitfun,
184        };
185        de.init();
186        de
187    }
188
189    fn init(&mut self) {
190        let dim = self.dim;
191        self.mean_hist = (0..10).map(|_| self.mean.clone()).collect();
192        self.mean_hist_index = 0;
193        if self.use_normal {
194            self.max_sigma = self
195                .sigma
196                .iter()
197                .map(|s| s / (0.1 + self.min_sigma_val))
198                .collect();
199            self.min_sigma_vec = self.sigma.iter().map(|s| self.min_sigma_val * s).collect();
200        }
201        self.pop_x = Vec::with_capacity(self.popsize);
202        self.pop_x0 = Vec::with_capacity(self.popsize);
203        self.pop_y = vec![f64::MAX; self.popsize];
204        for _ in 0..self.popsize {
205            let s = self.sample();
206            self.pop_x0.push(s.clone());
207            self.pop_x.push(s);
208        }
209        self.best_i = 0;
210        self.best_x = self.pop_x[0].clone();
211        self.pop_iter = vec![0; self.popsize];
212        self.asked_x = vec![vec![0.0; dim]; self.popsize];
213        self.asked_p = vec![0; self.popsize];
214    }
215
216    pub fn dim(&self) -> usize {
217        self.dim
218    }
219    pub fn popsize(&self) -> usize {
220        self.popsize
221    }
222    pub fn stop(&self) -> i32 {
223        self.stop
224    }
225
226    fn sample(&mut self) -> Vec<f64> {
227        if self.use_normal {
228            let raw: Vec<f64> = (0..self.dim)
229                .map(|i| self.mean[i] + self.rng.gaussian() * self.sigma[i])
230                .collect();
231            self.fitfun.closest_feasible(&raw)
232        } else {
233            self.fitfun.sample(&mut self.rng)
234        }
235    }
236
237    fn sample_i(&mut self, i: usize) -> f64 {
238        if self.use_normal {
239            let v = self.rng.normreal(self.mean[i], self.sigma[i]);
240            self.fitfun.closest_feasible_i(i, v)
241        } else {
242            self.fitfun.sample_i(i, &mut self.rng)
243        }
244    }
245
246    fn update_mean(&mut self) {
247        if !self.use_normal {
248            return;
249        }
250        self.mean_hist[self.mean_hist_index] = self.pop_x[self.best_i].clone();
251        self.mean_hist_index = (self.mean_hist_index + 1) % self.mean_hist.len();
252        // delta = rowwise (max - min) over the history columns; clamped per
253        // coordinate against parallel max/min-sigma arrays.
254        let mut sigma_new = vec![0.0; self.dim];
255        for (i, sn) in sigma_new.iter_mut().enumerate() {
256            let mut lo = f64::MAX;
257            let mut hi = f64::MIN;
258            for col in &self.mean_hist {
259                lo = lo.min(col[i]);
260                hi = hi.max(col[i]);
261            }
262            *sn = (hi - lo).min(self.max_sigma[i]).max(self.min_sigma_vec[i]);
263        }
264        let mean_new: f64 = sigma_new.iter().sum::<f64>() / self.dim as f64;
265        let mean_old: f64 = self.sigma.iter().sum::<f64>() / self.dim as f64;
266        if mean_new > mean_old {
267            self.sigma = sigma_new;
268        } else {
269            for (s, sn) in self.sigma.iter_mut().zip(&sigma_new) {
270                *s = 0.9 * *s + 0.1 * sn;
271            }
272        }
273        let best = self.pop_x[self.best_i].clone();
274        for (m, b) in self.mean.iter_mut().zip(&best) {
275            *m = 0.9 * *m + 0.1 * b;
276        }
277    }
278
279    fn modify(&mut self, x: &mut [f64]) {
280        let Some(is_int) = self.is_int.clone() else {
281            return;
282        };
283        let n_ints = is_int.iter().filter(|&&b| b).count() as f64;
284        if n_ints == 0.0 {
285            return;
286        }
287        let to_mutate =
288            self.min_mutate + self.rng.uniform01() * (self.max_mutate - self.min_mutate);
289        for i in 0..self.dim {
290            if is_int[i] && self.rng.uniform01() < to_mutate / n_ints {
291                x[i] = self.sample_i(i).trunc();
292            }
293        }
294    }
295
296    fn next_improve(&mut self, xb: &[f64], x: &[f64], xi: &[f64]) -> Vec<f64> {
297        let raw: Vec<f64> = (0..self.dim)
298            .map(|j| xb[j] + (x[j] - xi[j]) * self.f0)
299            .collect();
300        let mut nextx = self.fitfun.closest_feasible(&raw);
301        self.modify(&mut nextx);
302        nextx
303    }
304
305    fn oscillate(&mut self) {
306        self.cr = if self.iterations % 2 == 0 {
307            0.5 * self.cr0
308        } else {
309            self.cr0
310        };
311        self.f = if self.iterations % 2 == 0 {
312            0.5 * self.f0
313        } else {
314            self.f0
315        };
316    }
317
318    fn pick_two(&mut self, p: usize) -> (usize, usize) {
319        let mut r1;
320        loop {
321            r1 = self.rng.int_below(self.popsize as i64) as usize;
322            if r1 != p && r1 != self.best_i {
323                break;
324            }
325        }
326        let mut r2;
327        loop {
328            r2 = self.rng.int_below(self.popsize as i64) as usize;
329            if r2 != p && r2 != self.best_i && r2 != r1 {
330                break;
331            }
332        }
333        (r1, r2)
334    }
335
336    /// Ask-path donor construction (the C++ `nextX`).
337    fn next_x(&mut self, p: usize, xp: &[f64], xb: &[f64]) -> Vec<f64> {
338        if p == 0 {
339            self.iterations += 1;
340            self.oscillate();
341            if self.iterations > 2 {
342                self.update_mean();
343            }
344        }
345        let (r1, r2) = self.pick_two(p);
346        let x1 = self.pop_x[r1].clone();
347        let x2 = self.pop_x[r2].clone();
348        let mut x: Vec<f64> = (0..self.dim)
349            .map(|j| xb[j] + (x1[j] - x2[j]) * self.f)
350            .collect();
351        let r = self.rng.int_below(self.dim as i64) as usize;
352        for j in 0..self.dim {
353            if j != r && self.rng.uniform01() > self.cr {
354                x[j] = xp[j];
355            }
356        }
357        let mut nextx = self.fitfun.closest_feasible(&x);
358        self.modify(&mut nextx);
359        nextx
360    }
361
362    fn ask_one(&mut self) -> (usize, Vec<f64>) {
363        if self.improves_x.is_empty() {
364            let p = self.pos;
365            let xp = self.pop_x[p].clone();
366            let xb = self.pop_x[self.best_i].clone();
367            let x = self.next_x(p, &xp, &xb);
368            self.pos = (self.pos + 1) % self.popsize;
369            (p, x)
370        } else {
371            let p = self.improves_p.pop_front().unwrap();
372            let x = self.improves_x.pop_front().unwrap();
373            (p, x)
374        }
375    }
376
377    fn tell_one(&mut self, y: f64, x: &[f64], p: usize) -> i32 {
378        if y.is_finite() && y < self.pop_y[p] {
379            if self.iterations > 1 {
380                let xb = self.pop_x[self.best_i].clone();
381                let xi = self.pop_x0[p].clone();
382                let improved = self.next_improve(&xb, x, &xi);
383                self.improves_p.push_back(p);
384                self.improves_x.push_back(improved);
385            }
386            self.pop_x0[p] = self.pop_x[p].clone();
387            self.pop_x[p] = x.to_vec();
388            self.pop_y[p] = y;
389            self.pop_iter[p] = self.iterations;
390            if y < self.pop_y[self.best_i] {
391                self.best_i = p;
392                if y < self.best_y {
393                    self.best_y = y;
394                    self.best_x = x.to_vec();
395                    if self.stopfitness.is_finite() && self.best_y < self.stopfitness {
396                        self.stop = 1;
397                    }
398                }
399            }
400        } else if self.keep * self.rng.uniform01() < (self.iterations - self.pop_iter[p]) as f64 {
401            self.pop_x[p] = self.sample();
402            self.pop_y[p] = f64::MAX;
403        }
404        self.stop
405    }
406
407    /// Serial generational loop (the C++ `doOptimize`), the driver behind
408    /// `optimize_de` for `workers <= 1`.
409    pub fn optimize(&mut self, obj: &impl Objective) -> DeResult {
410        self.iterations = 1;
411        self.fitfun.reset_evaluations();
412        while self.fitfun.evaluations() < self.max_evaluations && !self.fitfun.terminate() {
413            if self.iterations > 2 {
414                self.update_mean();
415            }
416            self.oscillate();
417            for p in 0..self.popsize {
418                let xp = self.pop_x[p].clone();
419                let xb = self.pop_x[self.best_i].clone();
420                let (r1, r2) = self.pick_two(p);
421                let x1 = self.pop_x[r1].clone();
422                let x2 = self.pop_x[r2].clone();
423                let r = self.rng.int_below(self.dim as i64) as usize;
424                let mut x = xp.clone();
425                for j in 0..self.dim {
426                    if j == r || self.rng.uniform01() < self.cr {
427                        x[j] = xb[j] + self.f * (x1[j] - x2[j]);
428                        if !self.fitfun.feasible_i(j, x[j]) {
429                            x[j] = self.sample_i(j);
430                        }
431                    }
432                }
433                self.modify(&mut x);
434                let mut y = self.fitfun.eval_encoded_scalar(&x, obj);
435                if y.is_finite() && y < self.pop_y[p] {
436                    // temporal locality: an extra trial along the last move
437                    let x2t = self.next_improve(&xb, &x, &xp);
438                    let y2 = self.fitfun.eval_encoded_scalar(&x2t, obj);
439                    if y2.is_finite() && y2 < y {
440                        y = y2;
441                        x = x2t;
442                    }
443                    self.pop_x[p] = x.clone();
444                    self.pop_y[p] = y;
445                    self.pop_iter[p] = self.iterations;
446                    if y < self.pop_y[self.best_i] {
447                        self.best_i = p;
448                        if y < self.best_y {
449                            self.best_y = y;
450                            self.best_x = x.clone();
451                            if self.stopfitness.is_finite() && self.best_y < self.stopfitness {
452                                self.stop = 1;
453                                return self.make_result(self.fitfun.evaluations());
454                            }
455                        }
456                    }
457                } else if self.keep * self.rng.uniform01()
458                    < (self.iterations - self.pop_iter[p]) as f64
459                {
460                    self.pop_x[p] = self.sample();
461                    self.pop_y[p] = f64::MAX;
462                }
463            }
464            self.iterations += 1;
465        }
466        self.make_result(self.fitfun.evaluations())
467    }
468
469    fn make_result(&self, evaluations: u64) -> DeResult {
470        DeResult {
471            x: self.best_x.clone(),
472            y: self.best_y,
473            evaluations,
474            iterations: self.iterations,
475            stop: self.stop,
476        }
477    }
478
479    // ---- ask/tell interface (mirrors DeState::Impl) ----
480
481    /// Ask for a full population of candidate rows.
482    pub fn ask(&mut self) -> Vec<Vec<f64>> {
483        for i in 0..self.popsize {
484            let (p, x) = self.ask_one();
485            self.asked_p[i] = p;
486            self.asked_x[i] = x;
487        }
488        self.asked_x.clone()
489    }
490
491    /// Tell fitness values for the population returned by [`ask`](De::ask).
492    pub fn tell(&mut self, ys: &[f64]) -> i32 {
493        for (i, &y) in ys.iter().enumerate() {
494            let x = self.asked_x[i].clone();
495            self.tell_one(y, &x, self.asked_p[i]);
496        }
497        self.external_evaluations += ys.len() as u64;
498        self.stop
499    }
500
501    pub fn population(&self) -> Vec<Vec<f64>> {
502        self.pop_x.clone()
503    }
504
505    pub fn result(&self) -> DeResult {
506        self.make_result(self.external_evaluations)
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    fn sphere(x: &[f64]) -> f64 {
515        x.iter().map(|v| v * v).sum()
516    }
517    fn rosen(x: &[f64]) -> f64 {
518        (0..x.len() - 1)
519            .map(|i| 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2))
520            .sum()
521    }
522
523    fn optimize(obj: impl Objective, dim: usize, seed: u64, evals: u64) -> f64 {
524        let fit = Fitness::bounded(dim, 1, &vec![-5.0; dim], &vec![5.0; dim]);
525        let params = DeParams {
526            popsize: 31,
527            max_evaluations: evals,
528            seed,
529            ..Default::default()
530        };
531        let mut de = De::new(fit, &[], &[], None, &params);
532        de.optimize(&obj).y
533    }
534
535    #[test]
536    fn minimizes_sphere() {
537        assert!(optimize(sphere as fn(&[f64]) -> f64, 5, 1, 8000) < 1e-6);
538    }
539
540    #[test]
541    fn minimizes_rosenbrock() {
542        let mut v: Vec<f64> = (0..5)
543            .map(|s| optimize(rosen as fn(&[f64]) -> f64, 5, s, 12000))
544            .collect();
545        v.sort_by(|a, b| a.partial_cmp(b).unwrap());
546        assert!(v[2] < 1.0, "rosen median too large: {v:?}");
547    }
548
549    #[test]
550    fn ask_tell_converges() {
551        let fit = Fitness::bounded(5, 1, &[-5.0; 5], &[5.0; 5]);
552        let params = DeParams {
553            popsize: 24,
554            max_evaluations: 8000,
555            seed: 3,
556            ..Default::default()
557        };
558        let mut de = De::new(fit, &[], &[], None, &params);
559        for _ in 0..400 {
560            let pop = de.ask();
561            let ys: Vec<f64> = pop.iter().map(|x| sphere(x)).collect();
562            if de.tell(&ys) != 0 {
563                break;
564            }
565        }
566        assert!(de.result().y < 1e-3, "ask/tell did not converge");
567    }
568
569    #[test]
570    fn integer_modify_runs() {
571        let fit = Fitness::bounded(4, 1, &[-5.0; 4], &[5.0; 4]);
572        let params = DeParams {
573            popsize: 20,
574            max_evaluations: 4000,
575            seed: 5,
576            ..Default::default()
577        };
578        let ints = Some(vec![true, false, true, false]);
579        let mut de = De::new(fit, &[], &[], ints, &params);
580        let r = de.optimize(&(sphere as fn(&[f64]) -> f64));
581        assert!(r.evaluations > 0);
582    }
583
584    #[test]
585    fn normal_sampling_default_parameters_getters_and_early_stop() {
586        let fit = Fitness::bounded(2, 1, &[-1.0; 2], &[1.0; 2]);
587        let params = DeParams {
588            popsize: 0,
589            max_evaluations: 10,
590            keep: 0.0,
591            stop_fitness: 1.0,
592            f: 0.0,
593            cr: 0.0,
594            min_mutate: 0.0,
595            max_mutate: 0.0,
596            min_sigma: 0.1,
597            seed: 11,
598            runid: -2,
599        };
600        let mut optimizer = De::new(fit, &[0.0, 0.0], &[0.2, 0.2], None, &params);
601        assert_eq!(optimizer.dim(), 2);
602        assert_eq!(optimizer.popsize(), 30);
603        assert_eq!(optimizer.population().len(), 30);
604        assert_eq!(optimizer.stop(), 0);
605        let result = optimizer.optimize(&(sphere as fn(&[f64]) -> f64));
606        assert_eq!(result.stop, 1);
607        assert!(result.evaluations > 0);
608    }
609}