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::SliceRandom;
15use rand::rngs::StdRng;
16use rand::{Rng, SeedableRng};
17use scirs2_core::random::Random;
18
19/// Simplified Sobol sequence generator
20/// For production use, a full Sobol implementation with proper generating matrices would be preferred
21struct SobolState {
22    dimension: usize,
23    count: usize,
24    direction_numbers: Vec<Vec<u32>>,
25}
26
27impl SobolState {
28    fn new(dimension: usize) -> Self {
29        let mut direction_numbers = Vec::new();
30
31        // Initialize direction numbers for first few dimensions
32        // This is a simplified implementation - full Sobol needs proper generating matrices
33        for d in 0..dimension {
34            let mut dirs = Vec::new();
35            if d == 0 {
36                // First dimension uses powers of 2
37                for i in 0..32 {
38                    dirs.push(1u32 << (31 - i));
39                }
40            } else {
41                // Other dimensions use simple polynomial recurrence
42                // This is not optimal but provides better distribution than random
43                let base = (d + 1) as u32;
44                dirs.push(1u32 << 31);
45                for i in 1..32 {
46                    let prev = dirs[i - 1];
47                    dirs.push(prev ^ (prev >> base));
48                }
49            }
50            direction_numbers.push(dirs);
51        }
52
53        SobolState {
54            dimension,
55            count: 0,
56            direction_numbers,
57        }
58    }
59
60    fn next_point(&mut self) -> Vec<f64> {
61        self.count += 1;
62        let mut point = Vec::with_capacity(self.dimension);
63
64        for d in 0..self.dimension {
65            let mut x = 0u32;
66            let mut c = self.count;
67            let mut j = 0;
68
69            while c > 0 {
70                if (c & 1) == 1 {
71                    x ^= self.direction_numbers[d][j];
72                }
73                c >>= 1;
74                j += 1;
75            }
76
77            // Convert to [0, 1)
78            point.push(x as f64 / (1u64 << 32) as f64);
79        }
80
81        point
82    }
83}
84
85/// Options for Differential Evolution algorithm
86#[derive(Debug, Clone)]
87pub struct DifferentialEvolutionOptions {
88    /// Maximum number of generations
89    pub maxiter: usize,
90    /// Population size (as a multiple of the number of parameters or absolute size)
91    pub popsize: usize,
92    /// The tolerance for convergence
93    pub tol: f64,
94    /// The mutation coefficient (F) or tuple of (lower_bound, upper_bound)
95    pub mutation: (f64, f64),
96    /// The recombination coefficient (CR)
97    pub recombination: f64,
98    /// Whether to polish the best solution with local optimization
99    pub polish: bool,
100    /// Initial population method: "latinhypercube", "halton", "sobol", or "random"
101    pub init: String,
102    /// Absolute tolerance for convergence
103    pub atol: f64,
104    /// Strategy for updating the population: "immediate" or "deferred"
105    pub updating: String,
106    /// Random seed for reproducibility
107    pub seed: Option<u64>,
108    /// Initial guess
109    pub x0: Option<Array1<f64>>,
110    /// Parallel computation options
111    pub parallel: Option<ParallelOptions>,
112}
113
114impl Default for DifferentialEvolutionOptions {
115    fn default() -> Self {
116        Self {
117            maxiter: 1000,
118            popsize: 15,
119            tol: 0.01,
120            mutation: (0.5, 1.0),
121            recombination: 0.7,
122            polish: true,
123            init: "latinhypercube".to_string(),
124            atol: 0.0,
125            updating: "immediate".to_string(),
126            seed: None,
127            x0: None,
128            parallel: None,
129        }
130    }
131}
132
133/// Strategy names for mutation
134#[derive(Debug, Clone, Copy, PartialEq)]
135pub enum Strategy {
136    Best1Bin,
137    Best1Exp,
138    Rand1Bin,
139    Rand1Exp,
140    Best2Bin,
141    Best2Exp,
142    Rand2Bin,
143    Rand2Exp,
144    CurrentToBest1Bin,
145    CurrentToBest1Exp,
146}
147
148impl Strategy {
149    fn from_str(s: &str) -> Option<Self> {
150        match s {
151            "best1bin" => Some(Strategy::Best1Bin),
152            "best1exp" => Some(Strategy::Best1Exp),
153            "rand1bin" => Some(Strategy::Rand1Bin),
154            "rand1exp" => Some(Strategy::Rand1Exp),
155            "best2bin" => Some(Strategy::Best2Bin),
156            "best2exp" => Some(Strategy::Best2Exp),
157            "rand2bin" => Some(Strategy::Rand2Bin),
158            "rand2exp" => Some(Strategy::Rand2Exp),
159            "currenttobest1bin" => Some(Strategy::CurrentToBest1Bin),
160            "currenttobest1exp" => Some(Strategy::CurrentToBest1Exp),
161            _ => None,
162        }
163    }
164}
165
166/// Bounds for variables in differential evolution
167pub type Bounds = Vec<(f64, f64)>;
168
169/// Differential Evolution solver
170pub struct DifferentialEvolution<F>
171where
172    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
173{
174    func: F,
175    bounds: Bounds,
176    options: DifferentialEvolutionOptions,
177    strategy: Strategy,
178    ndim: usize,
179    population: Array2<f64>,
180    energies: Array1<f64>,
181    best_energy: f64,
182    best_idx: usize,
183    rng: Random<StdRng>,
184    nfev: usize,
185}
186
187use ndarray::Array2;
188
189impl<F> DifferentialEvolution<F>
190where
191    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
192{
193    /// Create new Differential Evolution solver
194    pub fn new(
195        func: F,
196        bounds: Bounds,
197        options: DifferentialEvolutionOptions,
198        strategy: &str,
199    ) -> Self {
200        let ndim = bounds.len();
201        let popsize = if options.popsize < ndim {
202            options.popsize * ndim
203        } else {
204            options.popsize
205        };
206
207        let seed = options.seed.unwrap_or_else(|| rand::rng().random());
208        let rng = Random::seed(seed);
209
210        let strategy_enum = Strategy::from_str(strategy).unwrap_or(Strategy::Best1Bin);
211
212        let mut solver = Self {
213            func,
214            bounds,
215            options,
216            strategy: strategy_enum,
217            ndim,
218            population: Array2::zeros((popsize, ndim)),
219            energies: Array1::zeros(popsize),
220            best_energy: f64::INFINITY,
221            best_idx: 0,
222            rng,
223            nfev: 0,
224        };
225
226        // Validate bounds
227        solver.validate_bounds();
228        solver.init_population();
229        solver
230    }
231
232    /// Validate that bounds are properly specified
233    fn validate_bounds(&self) {
234        for (i, &(lb, ub)) in self.bounds.iter().enumerate() {
235            if !lb.is_finite() || !ub.is_finite() {
236                panic!(
237                    "Bounds must be finite values. Variable {}: bounds = ({}, {})",
238                    i, lb, ub
239                );
240            }
241            if lb >= ub {
242                panic!(
243                    "Lower bound must be less than upper bound. Variable {}: lb = {}, ub = {}",
244                    i, lb, ub
245                );
246            }
247            if (ub - lb) < 1e-12 {
248                panic!(
249                    "Bounds range is too small. Variable {}: range = {}",
250                    i,
251                    ub - lb
252                );
253            }
254        }
255    }
256
257    /// Initialize the population
258    fn init_population(&mut self) {
259        let popsize = self.population.nrows();
260
261        // Initialize population based on initialization method
262        match self.options.init.as_str() {
263            "latinhypercube" => self.init_latinhypercube(),
264            "halton" => self.init_halton(),
265            "sobol" => self.init_sobol(),
266            _ => self.init_random(),
267        }
268
269        // If x0 is provided, replace one member with it (bounds-checked)
270        if let Some(x0) = self.options.x0.clone() {
271            for (i, &val) in x0.iter().enumerate() {
272                self.population[[0, i]] = self.ensure_bounds(i, val);
273            }
274        }
275
276        // Evaluate initial population
277        if self.options.parallel.is_some() {
278            // Parallel evaluation
279            let candidates: Vec<Array1<f64>> = (0..popsize)
280                .map(|i| self.population.row(i).to_owned())
281                .collect();
282
283            // Extract parallel options for evaluation
284            let parallel_opts = self.options.parallel.as_ref().unwrap();
285
286            let energies = parallel_evaluate_batch(&self.func, &candidates, parallel_opts);
287            self.energies = Array1::from_vec(energies);
288            self.nfev += popsize;
289
290            // Find best
291            for i in 0..popsize {
292                if self.energies[i] < self.best_energy {
293                    self.best_energy = self.energies[i];
294                    self.best_idx = i;
295                }
296            }
297        } else {
298            // Sequential evaluation
299            for i in 0..popsize {
300                let candidate = self.population.row(i);
301                self.energies[i] = (self.func)(&candidate);
302                self.nfev += 1;
303
304                if self.energies[i] < self.best_energy {
305                    self.best_energy = self.energies[i];
306                    self.best_idx = i;
307                }
308            }
309        }
310    }
311
312    /// Initialize population with random values
313    fn init_random(&mut self) {
314        let popsize = self.population.nrows();
315
316        for i in 0..popsize {
317            for j in 0..self.ndim {
318                let (lb, ub) = self.bounds[j];
319                let uniform = Uniform::new(lb, ub).unwrap();
320                self.population[[i, j]] = self.rng.sample(uniform);
321            }
322        }
323    }
324
325    /// Initialize population using Latin hypercube sampling
326    fn init_latinhypercube(&mut self) {
327        let popsize = self.population.nrows();
328
329        // Create segments for each dimension
330        for j in 0..self.ndim {
331            let (lb, ub) = self.bounds[j];
332            let segment_size = (ub - lb) / popsize as f64;
333
334            let mut segments: Vec<usize> = (0..popsize).collect();
335            segments.shuffle(&mut self.rng);
336
337            for (i, &seg) in segments.iter().enumerate() {
338                let segment_lb = lb + seg as f64 * segment_size;
339                let segment_ub = segment_lb + segment_size;
340                let uniform = Uniform::new(segment_lb, segment_ub).unwrap();
341                self.population[[i, j]] = self.rng.sample(uniform);
342            }
343        }
344    }
345
346    /// Initialize population using Halton sequence
347    fn init_halton(&mut self) {
348        let primes = vec![
349            2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83,
350            89, 97,
351        ];
352
353        let popsize = self.population.nrows();
354        for i in 0..popsize {
355            for j in 0..self.ndim {
356                // Use the (i+1)th term of the Halton sequence for base primes[j % primes.len()]
357                let base = primes[j % primes.len()];
358                let halton_value = self.halton_number(i + 1, base);
359
360                // Scale to bounds
361                let (lb, ub) = self.bounds[j];
362                self.population[[i, j]] = lb + halton_value * (ub - lb);
363            }
364        }
365    }
366
367    /// Initialize population using Sobol sequence
368    fn init_sobol(&mut self) {
369        // Simplified Sobol sequence using scrambled Van der Corput sequence
370        // For a full Sobol implementation, we would need generating matrices
371        let mut sobol_state = SobolState::new(self.ndim);
372
373        let popsize = self.population.nrows();
374        for i in 0..popsize {
375            let sobol_point = sobol_state.next_point();
376            for j in 0..self.ndim {
377                // Scale to bounds
378                let (lb, ub) = self.bounds[j];
379                let scaled_value = if j < sobol_point.len() {
380                    lb + sobol_point[j] * (ub - lb)
381                } else {
382                    // Fallback for higher dimensions
383                    let base = 2u32.pow((j + 1) as u32);
384                    let halton_value = self.halton_number(i + 1, base as usize);
385                    lb + halton_value * (ub - lb)
386                };
387                self.population[[i, j]] = scaled_value;
388            }
389        }
390    }
391
392    /// Generate the n-th number in the Halton sequence for given base
393    fn halton_number(&self, n: usize, base: usize) -> f64 {
394        let mut result = 0.0;
395        let mut f = 1.0 / base as f64;
396        let mut i = n;
397
398        while i > 0 {
399            result += f * (i % base) as f64;
400            i /= base;
401            f /= base as f64;
402        }
403
404        result
405    }
406
407    /// Ensure bounds for a parameter using reflection method
408    fn ensure_bounds(&mut self, idx: usize, val: f64) -> f64 {
409        let (lb, ub) = self.bounds[idx];
410
411        if val >= lb && val <= ub {
412            // Value is within bounds
413            val
414        } else if val < lb {
415            // Reflect around lower bound
416            let excess = lb - val;
417            let range = ub - lb;
418            if excess <= range {
419                lb + excess
420            } else {
421                // If reflection goes beyond upper bound, use random value in range
422                self.rng.gen_range(lb..ub)
423            }
424        } else {
425            // val > ub..reflect around upper bound
426            let excess = val - ub;
427            let range = ub - lb;
428            if excess <= range {
429                ub - excess
430            } else {
431                // If reflection goes beyond lower bound, use random value in range
432                self.rng.gen_range(lb..ub)
433            }
434        }
435    }
436
437    /// Create mutant vector using differential evolution
438    fn create_mutant(&mut self, candidate_idx: usize) -> Array1<f64> {
439        let popsize = self.population.nrows();
440        let mut mutant = Array1::zeros(self.ndim);
441
442        // Select indices for mutation
443        let mut indices: Vec<usize> = Vec::with_capacity(5);
444        while indices.len() < 5 {
445            let idx = self.rng.gen_range(0..popsize);
446            if idx != candidate_idx && !indices.contains(&idx) {
447                indices.push(idx);
448            }
449        }
450
451        let mutation_factor = if self.options.mutation.0 == self.options.mutation.1 {
452            self.options.mutation.0
453        } else {
454            self.rng
455                .gen_range(self.options.mutation.0..self.options.mutation.1)
456        };
457
458        match self.strategy {
459            Strategy::Best1Bin | Strategy::Best1Exp => {
460                // mutant = best + F * (r1 - r2)
461                let best = self.population.row(self.best_idx);
462                let r1 = self.population.row(indices[0]);
463                let r2 = self.population.row(indices[1]);
464                for i in 0..self.ndim {
465                    mutant[i] = best[i] + mutation_factor * (r1[i] - r2[i]);
466                }
467            }
468            Strategy::Rand1Bin | Strategy::Rand1Exp => {
469                // mutant = r0 + F * (r1 - r2)
470                let r0 = self.population.row(indices[0]);
471                let r1 = self.population.row(indices[1]);
472                let r2 = self.population.row(indices[2]);
473                for i in 0..self.ndim {
474                    mutant[i] = r0[i] + mutation_factor * (r1[i] - r2[i]);
475                }
476            }
477            Strategy::Best2Bin | Strategy::Best2Exp => {
478                // mutant = best + F * (r1 - r2) + F * (r3 - r4)
479                let best = self.population.row(self.best_idx);
480                let r1 = self.population.row(indices[0]);
481                let r2 = self.population.row(indices[1]);
482                let r3 = self.population.row(indices[2]);
483                let r4 = self.population.row(indices[3]);
484                for i in 0..self.ndim {
485                    mutant[i] = best[i]
486                        + mutation_factor * (r1[i] - r2[i])
487                        + mutation_factor * (r3[i] - r4[i]);
488                }
489            }
490            Strategy::Rand2Bin | Strategy::Rand2Exp => {
491                // mutant = r0 + F * (r1 - r2) + F * (r3 - r4)
492                let r0 = self.population.row(indices[0]);
493                let r1 = self.population.row(indices[1]);
494                let r2 = self.population.row(indices[2]);
495                let r3 = self.population.row(indices[3]);
496                let r4 = self.population.row(indices[4]);
497                for i in 0..self.ndim {
498                    mutant[i] = r0[i]
499                        + mutation_factor * (r1[i] - r2[i])
500                        + mutation_factor * (r3[i] - r4[i]);
501                }
502            }
503            Strategy::CurrentToBest1Bin | Strategy::CurrentToBest1Exp => {
504                // mutant = current + F * (best - current) + F * (r1 - r2)
505                let current = self.population.row(candidate_idx);
506                let best = self.population.row(self.best_idx);
507                let r1 = self.population.row(indices[0]);
508                let r2 = self.population.row(indices[1]);
509                for i in 0..self.ndim {
510                    mutant[i] = current[i]
511                        + mutation_factor * (best[i] - current[i])
512                        + mutation_factor * (r1[i] - r2[i]);
513                }
514            }
515        }
516
517        // Apply bounds checking after all calculations are done
518        for i in 0..self.ndim {
519            mutant[i] = self.ensure_bounds(i, mutant[i]);
520        }
521
522        mutant
523    }
524
525    /// Create trial vector using crossover
526    fn create_trial(&mut self, candidate_idx: usize, mutant: &Array1<f64>) -> Array1<f64> {
527        let candidate = self.population.row(candidate_idx).to_owned();
528        let mut trial = candidate.clone();
529
530        match self.strategy {
531            Strategy::Best1Bin
532            | Strategy::Rand1Bin
533            | Strategy::Best2Bin
534            | Strategy::Rand2Bin
535            | Strategy::CurrentToBest1Bin => {
536                // Binomial crossover
537                let randn = self.rng.gen_range(0..self.ndim);
538                for i in 0..self.ndim {
539                    if i == randn || self.rng.gen_range(0.0..1.0) < self.options.recombination {
540                        trial[i] = mutant[i];
541                    }
542                }
543            }
544            Strategy::Best1Exp
545            | Strategy::Rand1Exp
546            | Strategy::Best2Exp
547            | Strategy::Rand2Exp
548            | Strategy::CurrentToBest1Exp => {
549                // Exponential crossover
550                let randn = self.rng.gen_range(0..self.ndim);
551                let mut i = randn;
552                loop {
553                    trial[i] = mutant[i];
554                    i = (i + 1) % self.ndim;
555                    if i == randn || self.rng.gen_range(0.0..1.0) >= self.options.recombination {
556                        break;
557                    }
558                }
559            }
560        }
561
562        // Ensure all trial elements are within bounds after crossover
563        for i in 0..self.ndim {
564            trial[i] = self.ensure_bounds(i, trial[i]);
565        }
566
567        trial
568    }
569
570    /// Run one generation of the algorithm
571    fn evolve(&mut self) -> bool {
572        let popsize = self.population.nrows();
573        let mut converged = true;
574
575        if self.options.parallel.is_some() {
576            // First, generate all mutants and trials
577            let mut trials_and_indices: Vec<(Array1<f64>, usize)> = Vec::with_capacity(popsize);
578            for idx in 0..popsize {
579                let mutant = self.create_mutant(idx);
580                let trial = self.create_trial(idx, &mutant);
581                trials_and_indices.push((trial, idx));
582            }
583
584            // Extract just the trials for batch evaluation
585            let trials: Vec<Array1<f64>> = trials_and_indices
586                .iter()
587                .map(|(trial_, _)| trial_.clone())
588                .collect();
589
590            // Extract the parallel options for evaluation
591            let parallel_opts = self.options.parallel.as_ref().unwrap();
592
593            // Evaluate all trials in parallel
594            let trial_energies = parallel_evaluate_batch(&self.func, &trials, parallel_opts);
595            self.nfev += popsize;
596
597            // Process results
598            for ((trial, idx), trial_energy) in
599                trials_and_indices.into_iter().zip(trial_energies.iter())
600            {
601                if *trial_energy < self.energies[idx] && self.options.updating == "immediate" {
602                    for i in 0..self.ndim {
603                        self.population[[idx, i]] = trial[i];
604                    }
605                    self.energies[idx] = *trial_energy;
606
607                    if *trial_energy < self.best_energy {
608                        self.best_energy = *trial_energy;
609                        self.best_idx = idx;
610                    }
611                }
612
613                // Check convergence
614                let diff = (self.energies[idx] - self.best_energy).abs();
615                if diff > self.options.tol + self.options.atol {
616                    converged = false;
617                }
618            }
619        } else {
620            // Sequential evolution
621            for idx in 0..popsize {
622                let mutant = self.create_mutant(idx);
623                let trial = self.create_trial(idx, &mutant);
624
625                let trial_energy = (self.func)(&trial.view());
626                self.nfev += 1;
627
628                if trial_energy < self.energies[idx] && self.options.updating == "immediate" {
629                    for i in 0..self.ndim {
630                        self.population[[idx, i]] = trial[i];
631                    }
632                    self.energies[idx] = trial_energy;
633
634                    if trial_energy < self.best_energy {
635                        self.best_energy = trial_energy;
636                        self.best_idx = idx;
637                    }
638                }
639
640                // Check convergence
641                let diff = (self.energies[idx] - self.best_energy).abs();
642                if diff > self.options.tol + self.options.atol {
643                    converged = false;
644                }
645            }
646        }
647
648        converged
649    }
650
651    /// Run the differential evolution algorithm
652    pub fn run(&mut self) -> OptimizeResult<f64> {
653        let mut converged = false;
654        let mut nit = 0;
655
656        for _ in 0..self.options.maxiter {
657            converged = self.evolve();
658            nit += 1;
659
660            if converged {
661                break;
662            }
663        }
664
665        let mut result = OptimizeResult {
666            x: self.population.row(self.best_idx).to_owned(),
667            fun: self.best_energy,
668            nfev: self.nfev,
669            func_evals: self.nfev,
670            nit,
671            success: converged,
672            message: if converged {
673                "Optimization converged successfully"
674            } else {
675                "Maximum number of iterations reached"
676            }
677            .to_string(),
678            ..Default::default()
679        };
680
681        // Polish the result with local optimization if requested
682        if self.options.polish {
683            let bounds_vec: Vec<(f64, f64)> = self.bounds.clone();
684            let local_result = minimize(
685                |x| (self.func)(x),
686                &result.x.to_vec(),
687                Method::LBFGS,
688                Some(Options {
689                    bounds: Some(
690                        UnconstrainedBounds::from_vecs(
691                            bounds_vec.iter().map(|b| Some(b.0)).collect(),
692                            bounds_vec.iter().map(|b| Some(b.1)).collect(),
693                        )
694                        .unwrap(),
695                    ),
696                    ..Default::default()
697                }),
698            )
699            .unwrap();
700            if local_result.success && local_result.fun < result.fun {
701                // Ensure polished result respects bounds
702                let mut polished_x = local_result.x;
703                for (i, &(lb, ub)) in self.bounds.iter().enumerate() {
704                    polished_x[i] = polished_x[i].max(lb).min(ub);
705                }
706
707                // Only accept if still better after bounds enforcement
708                let polished_fun = (self.func)(&polished_x.view());
709                if polished_fun < result.fun {
710                    result.x = polished_x;
711                    result.fun = polished_fun;
712                    result.nfev += local_result.nfev + 1; // +1 for our re-evaluation
713                    result.func_evals = result.nfev;
714                }
715            }
716        }
717
718        result
719    }
720}
721
722/// Perform global optimization using differential evolution
723#[allow(dead_code)]
724pub fn differential_evolution<F>(
725    func: F,
726    bounds: Bounds,
727    options: Option<DifferentialEvolutionOptions>,
728    strategy: Option<&str>,
729) -> Result<OptimizeResult<f64>, OptimizeError>
730where
731    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
732{
733    let options = options.unwrap_or_default();
734    let strategy = strategy.unwrap_or("best1bin");
735
736    let mut solver = DifferentialEvolution::new(func, bounds, options, strategy);
737    Ok(solver.run())
738}