scirs2_optimize/global/
differential_evolution.rs

1//! Differential Evolution algorithm for global optimization
2//!
3//! The differential evolution method is a stochastic global optimization
4//! algorithm that does not use gradient methods to find the minimum and
5//! can search large areas of candidate space.
6
7use crate::error::OptimizeError;
8use crate::parallel::{parallel_evaluate_batch, ParallelOptions};
9use crate::unconstrained::{
10    minimize, Bounds as UnconstrainedBounds, Method, OptimizeResult, Options,
11};
12use ndarray::{Array1, ArrayView1};
13use rand::distr::Uniform;
14use rand::prelude::*;
15use rand::rngs::StdRng;
16
17/// Options for Differential Evolution algorithm
18#[derive(Debug, Clone)]
19pub struct DifferentialEvolutionOptions {
20    /// Maximum number of generations
21    pub maxiter: usize,
22    /// Population size (as a multiple of the number of parameters or absolute size)
23    pub popsize: usize,
24    /// The tolerance for convergence
25    pub tol: f64,
26    /// The mutation coefficient (F) or tuple of (lower_bound, upper_bound)
27    pub mutation: (f64, f64),
28    /// The recombination coefficient (CR)
29    pub recombination: f64,
30    /// Whether to polish the best solution with local optimization
31    pub polish: bool,
32    /// Initial population method: "latinhypercube", "halton", "sobol", or "random"
33    pub init: String,
34    /// Absolute tolerance for convergence
35    pub atol: f64,
36    /// Strategy for updating the population: "immediate" or "deferred"
37    pub updating: String,
38    /// Random seed for reproducibility
39    pub seed: Option<u64>,
40    /// Initial guess
41    pub x0: Option<Array1<f64>>,
42    /// Parallel computation options
43    pub parallel: Option<ParallelOptions>,
44}
45
46impl Default for DifferentialEvolutionOptions {
47    fn default() -> Self {
48        Self {
49            maxiter: 1000,
50            popsize: 15,
51            tol: 0.01,
52            mutation: (0.5, 1.0),
53            recombination: 0.7,
54            polish: true,
55            init: "latinhypercube".to_string(),
56            atol: 0.0,
57            updating: "immediate".to_string(),
58            seed: None,
59            x0: None,
60            parallel: None,
61        }
62    }
63}
64
65/// Strategy names for mutation
66#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum Strategy {
68    Best1Bin,
69    Best1Exp,
70    Rand1Bin,
71    Rand1Exp,
72    Best2Bin,
73    Best2Exp,
74    Rand2Bin,
75    Rand2Exp,
76    CurrentToBest1Bin,
77    CurrentToBest1Exp,
78}
79
80impl Strategy {
81    fn from_str(s: &str) -> Option<Self> {
82        match s {
83            "best1bin" => Some(Strategy::Best1Bin),
84            "best1exp" => Some(Strategy::Best1Exp),
85            "rand1bin" => Some(Strategy::Rand1Bin),
86            "rand1exp" => Some(Strategy::Rand1Exp),
87            "best2bin" => Some(Strategy::Best2Bin),
88            "best2exp" => Some(Strategy::Best2Exp),
89            "rand2bin" => Some(Strategy::Rand2Bin),
90            "rand2exp" => Some(Strategy::Rand2Exp),
91            "currenttobest1bin" => Some(Strategy::CurrentToBest1Bin),
92            "currenttobest1exp" => Some(Strategy::CurrentToBest1Exp),
93            _ => None,
94        }
95    }
96}
97
98/// Bounds for variables in differential evolution
99pub type Bounds = Vec<(f64, f64)>;
100
101/// Differential Evolution solver
102pub struct DifferentialEvolution<F>
103where
104    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
105{
106    func: F,
107    bounds: Bounds,
108    options: DifferentialEvolutionOptions,
109    strategy: Strategy,
110    ndim: usize,
111    population: Array2<f64>,
112    energies: Array1<f64>,
113    best_energy: f64,
114    best_idx: usize,
115    rng: StdRng,
116    nfev: usize,
117}
118
119use ndarray::Array2;
120
121impl<F> DifferentialEvolution<F>
122where
123    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
124{
125    /// Create new Differential Evolution solver
126    pub fn new(
127        func: F,
128        bounds: Bounds,
129        options: DifferentialEvolutionOptions,
130        strategy: &str,
131    ) -> Self {
132        let ndim = bounds.len();
133        let popsize = if options.popsize < ndim {
134            options.popsize * ndim
135        } else {
136            options.popsize
137        };
138
139        let seed = options.seed.unwrap_or_else(rand::random);
140        let rng = StdRng::seed_from_u64(seed);
141
142        let strategy_enum = Strategy::from_str(strategy).unwrap_or(Strategy::Best1Bin);
143
144        let mut solver = Self {
145            func,
146            bounds,
147            options,
148            strategy: strategy_enum,
149            ndim,
150            population: Array2::zeros((popsize, ndim)),
151            energies: Array1::zeros(popsize),
152            best_energy: f64::INFINITY,
153            best_idx: 0,
154            rng,
155            nfev: 0,
156        };
157
158        solver.init_population();
159        solver
160    }
161
162    /// Initialize the population
163    fn init_population(&mut self) {
164        let popsize = self.population.nrows();
165
166        // Initialize population based on initialization method
167        match self.options.init.as_str() {
168            "latinhypercube" => self.init_latinhypercube(),
169            "halton" => self.init_halton(),
170            "sobol" => self.init_sobol(),
171            _ => self.init_random(),
172        }
173
174        // If x0 is provided, replace one member with it
175        if let Some(ref x0) = self.options.x0 {
176            for (i, &val) in x0.iter().enumerate() {
177                self.population[[0, i]] = val;
178            }
179        }
180
181        // Evaluate initial population
182        if self.options.parallel.is_some() {
183            // Parallel evaluation
184            let candidates: Vec<Array1<f64>> = (0..popsize)
185                .map(|i| self.population.row(i).to_owned())
186                .collect();
187
188            // Extract parallel options for evaluation
189            let parallel_opts = self.options.parallel.as_ref().unwrap();
190
191            let energies = parallel_evaluate_batch(&self.func, &candidates, parallel_opts);
192            self.energies = Array1::from_vec(energies);
193            self.nfev += popsize;
194
195            // Find best
196            for i in 0..popsize {
197                if self.energies[i] < self.best_energy {
198                    self.best_energy = self.energies[i];
199                    self.best_idx = i;
200                }
201            }
202        } else {
203            // Sequential evaluation
204            for i in 0..popsize {
205                let candidate = self.population.row(i);
206                self.energies[i] = (self.func)(&candidate);
207                self.nfev += 1;
208
209                if self.energies[i] < self.best_energy {
210                    self.best_energy = self.energies[i];
211                    self.best_idx = i;
212                }
213            }
214        }
215    }
216
217    /// Initialize population with random values
218    fn init_random(&mut self) {
219        let popsize = self.population.nrows();
220
221        for i in 0..popsize {
222            for j in 0..self.ndim {
223                let (lb, ub) = self.bounds[j];
224                let uniform = Uniform::new(lb, ub).unwrap();
225                self.population[[i, j]] = self.rng.sample(uniform);
226            }
227        }
228    }
229
230    /// Initialize population using Latin hypercube sampling
231    fn init_latinhypercube(&mut self) {
232        let popsize = self.population.nrows();
233
234        // Create segments for each dimension
235        for j in 0..self.ndim {
236            let (lb, ub) = self.bounds[j];
237            let segment_size = (ub - lb) / popsize as f64;
238
239            let mut segments: Vec<usize> = (0..popsize).collect();
240            segments.shuffle(&mut self.rng);
241
242            for (i, &seg) in segments.iter().enumerate() {
243                let segment_lb = lb + seg as f64 * segment_size;
244                let segment_ub = segment_lb + segment_size;
245                let uniform = Uniform::new(segment_lb, segment_ub).unwrap();
246                self.population[[i, j]] = self.rng.sample(uniform);
247            }
248        }
249    }
250
251    /// Initialize population using Halton sequence
252    fn init_halton(&mut self) {
253        // Simplified Halton sequence implementation
254        self.init_random(); // Fallback to random for now
255    }
256
257    /// Initialize population using Sobol sequence
258    fn init_sobol(&mut self) {
259        // Simplified Sobol sequence implementation
260        self.init_random(); // Fallback to random for now
261    }
262
263    /// Ensure bounds for a parameter
264    fn ensure_bounds(&self, idx: usize, val: f64) -> f64 {
265        let (lb, ub) = self.bounds[idx];
266        val.max(lb).min(ub)
267    }
268
269    /// Create mutant vector using differential evolution
270    fn create_mutant(&mut self, candidate_idx: usize) -> Array1<f64> {
271        let popsize = self.population.nrows();
272        let mut mutant = Array1::zeros(self.ndim);
273
274        // Select indices for mutation
275        let mut indices: Vec<usize> = Vec::with_capacity(5);
276        while indices.len() < 5 {
277            let idx = self.rng.random_range(0..popsize);
278            if idx != candidate_idx && !indices.contains(&idx) {
279                indices.push(idx);
280            }
281        }
282
283        let mutation_factor = if self.options.mutation.0 == self.options.mutation.1 {
284            self.options.mutation.0
285        } else {
286            self.rng
287                .random_range(self.options.mutation.0..self.options.mutation.1)
288        };
289
290        match self.strategy {
291            Strategy::Best1Bin | Strategy::Best1Exp => {
292                // mutant = best + F * (r1 - r2)
293                let best = self.population.row(self.best_idx);
294                let r1 = self.population.row(indices[0]);
295                let r2 = self.population.row(indices[1]);
296                for i in 0..self.ndim {
297                    mutant[i] = best[i] + mutation_factor * (r1[i] - r2[i]);
298                    mutant[i] = self.ensure_bounds(i, mutant[i]);
299                }
300            }
301            Strategy::Rand1Bin | Strategy::Rand1Exp => {
302                // mutant = r0 + F * (r1 - r2)
303                let r0 = self.population.row(indices[0]);
304                let r1 = self.population.row(indices[1]);
305                let r2 = self.population.row(indices[2]);
306                for i in 0..self.ndim {
307                    mutant[i] = r0[i] + mutation_factor * (r1[i] - r2[i]);
308                    mutant[i] = self.ensure_bounds(i, mutant[i]);
309                }
310            }
311            Strategy::Best2Bin | Strategy::Best2Exp => {
312                // mutant = best + F * (r1 - r2) + F * (r3 - r4)
313                let best = self.population.row(self.best_idx);
314                let r1 = self.population.row(indices[0]);
315                let r2 = self.population.row(indices[1]);
316                let r3 = self.population.row(indices[2]);
317                let r4 = self.population.row(indices[3]);
318                for i in 0..self.ndim {
319                    mutant[i] = best[i]
320                        + mutation_factor * (r1[i] - r2[i])
321                        + mutation_factor * (r3[i] - r4[i]);
322                    mutant[i] = self.ensure_bounds(i, mutant[i]);
323                }
324            }
325            Strategy::Rand2Bin | Strategy::Rand2Exp => {
326                // mutant = r0 + F * (r1 - r2) + F * (r3 - r4)
327                let r0 = self.population.row(indices[0]);
328                let r1 = self.population.row(indices[1]);
329                let r2 = self.population.row(indices[2]);
330                let r3 = self.population.row(indices[3]);
331                let r4 = self.population.row(indices[4]);
332                for i in 0..self.ndim {
333                    mutant[i] = r0[i]
334                        + mutation_factor * (r1[i] - r2[i])
335                        + mutation_factor * (r3[i] - r4[i]);
336                    mutant[i] = self.ensure_bounds(i, mutant[i]);
337                }
338            }
339            Strategy::CurrentToBest1Bin | Strategy::CurrentToBest1Exp => {
340                // mutant = current + F * (best - current) + F * (r1 - r2)
341                let current = self.population.row(candidate_idx);
342                let best = self.population.row(self.best_idx);
343                let r1 = self.population.row(indices[0]);
344                let r2 = self.population.row(indices[1]);
345                for i in 0..self.ndim {
346                    mutant[i] = current[i]
347                        + mutation_factor * (best[i] - current[i])
348                        + mutation_factor * (r1[i] - r2[i]);
349                    mutant[i] = self.ensure_bounds(i, mutant[i]);
350                }
351            }
352        }
353
354        mutant
355    }
356
357    /// Create trial vector using crossover
358    fn create_trial(&mut self, candidate_idx: usize, mutant: &Array1<f64>) -> Array1<f64> {
359        let candidate = self.population.row(candidate_idx).to_owned();
360        let mut trial = candidate.clone();
361
362        match self.strategy {
363            Strategy::Best1Bin
364            | Strategy::Rand1Bin
365            | Strategy::Best2Bin
366            | Strategy::Rand2Bin
367            | Strategy::CurrentToBest1Bin => {
368                // Binomial crossover
369                let randn = self.rng.random_range(0..self.ndim);
370                for i in 0..self.ndim {
371                    if i == randn || self.rng.random::<f64>() < self.options.recombination {
372                        trial[i] = mutant[i];
373                    }
374                }
375            }
376            Strategy::Best1Exp
377            | Strategy::Rand1Exp
378            | Strategy::Best2Exp
379            | Strategy::Rand2Exp
380            | Strategy::CurrentToBest1Exp => {
381                // Exponential crossover
382                let randn = self.rng.random_range(0..self.ndim);
383                let mut i = randn;
384                loop {
385                    trial[i] = mutant[i];
386                    i = (i + 1) % self.ndim;
387                    if i == randn || self.rng.random::<f64>() >= self.options.recombination {
388                        break;
389                    }
390                }
391            }
392        }
393
394        trial
395    }
396
397    /// Run one generation of the algorithm
398    fn evolve(&mut self) -> bool {
399        let popsize = self.population.nrows();
400        let mut converged = true;
401
402        if self.options.parallel.is_some() {
403            // First, generate all mutants and trials
404            let mut trials_and_indices: Vec<(Array1<f64>, usize)> = Vec::with_capacity(popsize);
405            for idx in 0..popsize {
406                let mutant = self.create_mutant(idx);
407                let trial = self.create_trial(idx, &mutant);
408                trials_and_indices.push((trial, idx));
409            }
410
411            // Extract just the trials for batch evaluation
412            let trials: Vec<Array1<f64>> = trials_and_indices
413                .iter()
414                .map(|(trial, _)| trial.clone())
415                .collect();
416
417            // Extract the parallel options for evaluation
418            let parallel_opts = self.options.parallel.as_ref().unwrap();
419
420            // Evaluate all trials in parallel
421            let trial_energies = parallel_evaluate_batch(&self.func, &trials, parallel_opts);
422            self.nfev += popsize;
423
424            // Process results
425            for ((trial, idx), trial_energy) in
426                trials_and_indices.into_iter().zip(trial_energies.iter())
427            {
428                if *trial_energy < self.energies[idx] && self.options.updating == "immediate" {
429                    for i in 0..self.ndim {
430                        self.population[[idx, i]] = trial[i];
431                    }
432                    self.energies[idx] = *trial_energy;
433
434                    if *trial_energy < self.best_energy {
435                        self.best_energy = *trial_energy;
436                        self.best_idx = idx;
437                    }
438                }
439
440                // Check convergence
441                let diff = (self.energies[idx] - self.best_energy).abs();
442                if diff > self.options.tol + self.options.atol {
443                    converged = false;
444                }
445            }
446        } else {
447            // Sequential evolution
448            for idx in 0..popsize {
449                let mutant = self.create_mutant(idx);
450                let trial = self.create_trial(idx, &mutant);
451
452                let trial_energy = (self.func)(&trial.view());
453                self.nfev += 1;
454
455                if trial_energy < self.energies[idx] && self.options.updating == "immediate" {
456                    for i in 0..self.ndim {
457                        self.population[[idx, i]] = trial[i];
458                    }
459                    self.energies[idx] = trial_energy;
460
461                    if trial_energy < self.best_energy {
462                        self.best_energy = trial_energy;
463                        self.best_idx = idx;
464                    }
465                }
466
467                // Check convergence
468                let diff = (self.energies[idx] - self.best_energy).abs();
469                if diff > self.options.tol + self.options.atol {
470                    converged = false;
471                }
472            }
473        }
474
475        converged
476    }
477
478    /// Run the differential evolution algorithm
479    pub fn run(&mut self) -> OptimizeResult<f64> {
480        let mut converged = false;
481        let mut nit = 0;
482
483        for _ in 0..self.options.maxiter {
484            converged = self.evolve();
485            nit += 1;
486
487            if converged {
488                break;
489            }
490        }
491
492        let mut result = OptimizeResult {
493            x: self.population.row(self.best_idx).to_owned(),
494            fun: self.best_energy,
495            nfev: self.nfev,
496            func_evals: self.nfev,
497            nit,
498            iterations: nit,
499            success: converged,
500            message: if converged {
501                "Optimization converged successfully"
502            } else {
503                "Maximum number of iterations reached"
504            }
505            .to_string(),
506            ..Default::default()
507        };
508
509        // Polish the result with local optimization if requested
510        if self.options.polish {
511            let bounds_vec: Vec<(f64, f64)> = self.bounds.clone();
512            let local_result = minimize(
513                |x| (self.func)(x),
514                &result.x.to_vec(),
515                Method::LBFGS,
516                Some(Options {
517                    bounds: Some(
518                        UnconstrainedBounds::from_vecs(
519                            bounds_vec.iter().map(|b| Some(b.0)).collect(),
520                            bounds_vec.iter().map(|b| Some(b.1)).collect(),
521                        )
522                        .unwrap(),
523                    ),
524                    ..Default::default()
525                }),
526            )
527            .unwrap();
528            if local_result.success && local_result.fun < result.fun {
529                result.x = local_result.x;
530                result.fun = local_result.fun;
531                result.nfev += local_result.nfev;
532                result.func_evals = result.nfev;
533            }
534        }
535
536        result
537    }
538}
539
540/// Perform global optimization using differential evolution
541pub fn differential_evolution<F>(
542    func: F,
543    bounds: Bounds,
544    options: Option<DifferentialEvolutionOptions>,
545    strategy: Option<&str>,
546) -> Result<OptimizeResult<f64>, OptimizeError>
547where
548    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
549{
550    let options = options.unwrap_or_default();
551    let strategy = strategy.unwrap_or("best1bin");
552
553    let mut solver = DifferentialEvolution::new(func, bounds, options, strategy);
554    Ok(solver.run())
555}