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 scirs2_core::ndarray::{Array1, ArrayView1};
13use scirs2_core::random::prelude::SliceRandom;
14use scirs2_core::random::rngs::StdRng;
15use scirs2_core::random::Random;
16use scirs2_core::random::Uniform;
17use scirs2_core::random::{Rng, SeedableRng};
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 scirs2_core::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
208            .seed
209            .unwrap_or_else(|| scirs2_core::random::rng().random());
210        let rng = Random::seed(seed);
211
212        let strategy_enum = Strategy::from_str(strategy).unwrap_or(Strategy::Best1Bin);
213
214        let mut solver = Self {
215            func,
216            bounds,
217            options,
218            strategy: strategy_enum,
219            ndim,
220            population: Array2::zeros((popsize, ndim)),
221            energies: Array1::zeros(popsize),
222            best_energy: f64::INFINITY,
223            best_idx: 0,
224            rng,
225            nfev: 0,
226        };
227
228        // Validate bounds
229        solver.validate_bounds();
230        solver.init_population();
231        solver
232    }
233
234    /// Validate that bounds are properly specified
235    fn validate_bounds(&self) {
236        for (i, &(lb, ub)) in self.bounds.iter().enumerate() {
237            if !lb.is_finite() || !ub.is_finite() {
238                panic!(
239                    "Bounds must be finite values. Variable {}: bounds = ({}, {})",
240                    i, lb, ub
241                );
242            }
243            if lb >= ub {
244                panic!(
245                    "Lower bound must be less than upper bound. Variable {}: lb = {}, ub = {}",
246                    i, lb, ub
247                );
248            }
249            if (ub - lb) < 1e-12 {
250                panic!(
251                    "Bounds range is too small. Variable {}: range = {}",
252                    i,
253                    ub - lb
254                );
255            }
256        }
257    }
258
259    /// Initialize the population
260    fn init_population(&mut self) {
261        let popsize = self.population.nrows();
262
263        // Initialize population based on initialization method
264        match self.options.init.as_str() {
265            "latinhypercube" => self.init_latinhypercube(),
266            "halton" => self.init_halton(),
267            "sobol" => self.init_sobol(),
268            _ => self.init_random(),
269        }
270
271        // If x0 is provided, replace one member with it (bounds-checked)
272        if let Some(x0) = self.options.x0.clone() {
273            for (i, &val) in x0.iter().enumerate() {
274                self.population[[0, i]] = self.ensure_bounds(i, val);
275            }
276        }
277
278        // Evaluate initial population
279        if self.options.parallel.is_some() {
280            // Parallel evaluation
281            let candidates: Vec<Array1<f64>> = (0..popsize)
282                .map(|i| self.population.row(i).to_owned())
283                .collect();
284
285            // Extract parallel options for evaluation
286            let parallel_opts = self.options.parallel.as_ref().unwrap();
287
288            let energies = parallel_evaluate_batch(&self.func, &candidates, parallel_opts);
289            self.energies = Array1::from_vec(energies);
290            self.nfev += popsize;
291
292            // Find best
293            for i in 0..popsize {
294                if self.energies[i] < self.best_energy {
295                    self.best_energy = self.energies[i];
296                    self.best_idx = i;
297                }
298            }
299        } else {
300            // Sequential evaluation
301            for i in 0..popsize {
302                let candidate = self.population.row(i);
303                self.energies[i] = (self.func)(&candidate);
304                self.nfev += 1;
305
306                if self.energies[i] < self.best_energy {
307                    self.best_energy = self.energies[i];
308                    self.best_idx = i;
309                }
310            }
311        }
312    }
313
314    /// Initialize population with random values
315    fn init_random(&mut self) {
316        let popsize = self.population.nrows();
317
318        for i in 0..popsize {
319            for j in 0..self.ndim {
320                let (lb, ub) = self.bounds[j];
321                let uniform = Uniform::new(lb, ub).unwrap();
322                self.population[[i, j]] = self.rng.sample(uniform);
323            }
324        }
325    }
326
327    /// Initialize population using Latin hypercube sampling
328    fn init_latinhypercube(&mut self) {
329        let popsize = self.population.nrows();
330
331        // Create segments for each dimension
332        for j in 0..self.ndim {
333            let (lb, ub) = self.bounds[j];
334            let segment_size = (ub - lb) / popsize as f64;
335
336            let mut segments: Vec<usize> = (0..popsize).collect();
337            segments.shuffle(&mut self.rng);
338
339            for (i, &seg) in segments.iter().enumerate() {
340                let segment_lb = lb + seg as f64 * segment_size;
341                let segment_ub = segment_lb + segment_size;
342                let uniform = Uniform::new(segment_lb, segment_ub).unwrap();
343                self.population[[i, j]] = self.rng.sample(uniform);
344            }
345        }
346    }
347
348    /// Initialize population using Halton sequence
349    fn init_halton(&mut self) {
350        let primes = [
351            2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83,
352            89, 97,
353        ];
354
355        let popsize = self.population.nrows();
356        for i in 0..popsize {
357            for j in 0..self.ndim {
358                // Use the (i+1)th term of the Halton sequence for base primes[j % primes.len()]
359                let base = primes[j % primes.len()];
360                let halton_value = self.halton_number(i + 1, base);
361
362                // Scale to bounds
363                let (lb, ub) = self.bounds[j];
364                self.population[[i, j]] = lb + halton_value * (ub - lb);
365            }
366        }
367    }
368
369    /// Initialize population using Sobol sequence
370    fn init_sobol(&mut self) {
371        // Simplified Sobol sequence using scrambled Van der Corput sequence
372        // For a full Sobol implementation, we would need generating matrices
373        let mut sobol_state = SobolState::new(self.ndim);
374
375        let popsize = self.population.nrows();
376        for i in 0..popsize {
377            let sobol_point = sobol_state.next_point();
378            for j in 0..self.ndim {
379                // Scale to bounds
380                let (lb, ub) = self.bounds[j];
381                let scaled_value = if j < sobol_point.len() {
382                    lb + sobol_point[j] * (ub - lb)
383                } else {
384                    // Fallback for higher dimensions
385                    let base = 2u32.pow((j + 1) as u32);
386                    let halton_value = self.halton_number(i + 1, base as usize);
387                    lb + halton_value * (ub - lb)
388                };
389                self.population[[i, j]] = scaled_value;
390            }
391        }
392    }
393
394    /// Generate the n-th number in the Halton sequence for given base
395    fn halton_number(&self, n: usize, base: usize) -> f64 {
396        let mut result = 0.0;
397        let mut f = 1.0 / base as f64;
398        let mut i = n;
399
400        while i > 0 {
401            result += f * (i % base) as f64;
402            i /= base;
403            f /= base as f64;
404        }
405
406        result
407    }
408
409    /// Ensure bounds for a parameter using reflection method
410    fn ensure_bounds(&mut self, idx: usize, val: f64) -> f64 {
411        let (lb, ub) = self.bounds[idx];
412
413        if val >= lb && val <= ub {
414            // Value is within bounds
415            val
416        } else if val < lb {
417            // Reflect around lower bound
418            let excess = lb - val;
419            let range = ub - lb;
420            if excess <= range {
421                lb + excess
422            } else {
423                // If reflection goes beyond upper bound, use random value in range
424                self.rng.gen_range(lb..ub)
425            }
426        } else {
427            // val > ub..reflect around upper bound
428            let excess = val - ub;
429            let range = ub - lb;
430            if excess <= range {
431                ub - excess
432            } else {
433                // If reflection goes beyond lower bound, use random value in range
434                self.rng.gen_range(lb..ub)
435            }
436        }
437    }
438
439    /// Create mutant vector using differential evolution
440    fn create_mutant(&mut self, candidate_idx: usize) -> Array1<f64> {
441        let popsize = self.population.nrows();
442        let mut mutant = Array1::zeros(self.ndim);
443
444        // Select indices for mutation
445        let mut indices: Vec<usize> = Vec::with_capacity(5);
446        while indices.len() < 5 {
447            let idx = self.rng.gen_range(0..popsize);
448            if idx != candidate_idx && !indices.contains(&idx) {
449                indices.push(idx);
450            }
451        }
452
453        let mutation_factor = if self.options.mutation.0 == self.options.mutation.1 {
454            self.options.mutation.0
455        } else {
456            self.rng
457                .gen_range(self.options.mutation.0..self.options.mutation.1)
458        };
459
460        match self.strategy {
461            Strategy::Best1Bin | Strategy::Best1Exp => {
462                // mutant = best + F * (r1 - r2)
463                let best = self.population.row(self.best_idx);
464                let r1 = self.population.row(indices[0]);
465                let r2 = self.population.row(indices[1]);
466                for i in 0..self.ndim {
467                    mutant[i] = best[i] + mutation_factor * (r1[i] - r2[i]);
468                }
469            }
470            Strategy::Rand1Bin | Strategy::Rand1Exp => {
471                // mutant = r0 + F * (r1 - r2)
472                let r0 = self.population.row(indices[0]);
473                let r1 = self.population.row(indices[1]);
474                let r2 = self.population.row(indices[2]);
475                for i in 0..self.ndim {
476                    mutant[i] = r0[i] + mutation_factor * (r1[i] - r2[i]);
477                }
478            }
479            Strategy::Best2Bin | Strategy::Best2Exp => {
480                // mutant = best + F * (r1 - r2) + F * (r3 - r4)
481                let best = self.population.row(self.best_idx);
482                let r1 = self.population.row(indices[0]);
483                let r2 = self.population.row(indices[1]);
484                let r3 = self.population.row(indices[2]);
485                let r4 = self.population.row(indices[3]);
486                for i in 0..self.ndim {
487                    mutant[i] = best[i]
488                        + mutation_factor * (r1[i] - r2[i])
489                        + mutation_factor * (r3[i] - r4[i]);
490                }
491            }
492            Strategy::Rand2Bin | Strategy::Rand2Exp => {
493                // mutant = r0 + F * (r1 - r2) + F * (r3 - r4)
494                let r0 = self.population.row(indices[0]);
495                let r1 = self.population.row(indices[1]);
496                let r2 = self.population.row(indices[2]);
497                let r3 = self.population.row(indices[3]);
498                let r4 = self.population.row(indices[4]);
499                for i in 0..self.ndim {
500                    mutant[i] = r0[i]
501                        + mutation_factor * (r1[i] - r2[i])
502                        + mutation_factor * (r3[i] - r4[i]);
503                }
504            }
505            Strategy::CurrentToBest1Bin | Strategy::CurrentToBest1Exp => {
506                // mutant = current + F * (best - current) + F * (r1 - r2)
507                let current = self.population.row(candidate_idx);
508                let best = self.population.row(self.best_idx);
509                let r1 = self.population.row(indices[0]);
510                let r2 = self.population.row(indices[1]);
511                for i in 0..self.ndim {
512                    mutant[i] = current[i]
513                        + mutation_factor * (best[i] - current[i])
514                        + mutation_factor * (r1[i] - r2[i]);
515                }
516            }
517        }
518
519        // Apply bounds checking after all calculations are done
520        for i in 0..self.ndim {
521            mutant[i] = self.ensure_bounds(i, mutant[i]);
522        }
523
524        mutant
525    }
526
527    /// Create trial vector using crossover
528    fn create_trial(&mut self, candidate_idx: usize, mutant: &Array1<f64>) -> Array1<f64> {
529        let candidate = self.population.row(candidate_idx).to_owned();
530        let mut trial = candidate.clone();
531
532        match self.strategy {
533            Strategy::Best1Bin
534            | Strategy::Rand1Bin
535            | Strategy::Best2Bin
536            | Strategy::Rand2Bin
537            | Strategy::CurrentToBest1Bin => {
538                // Binomial crossover
539                let randn = self.rng.gen_range(0..self.ndim);
540                for i in 0..self.ndim {
541                    if i == randn || self.rng.gen_range(0.0..1.0) < self.options.recombination {
542                        trial[i] = mutant[i];
543                    }
544                }
545            }
546            Strategy::Best1Exp
547            | Strategy::Rand1Exp
548            | Strategy::Best2Exp
549            | Strategy::Rand2Exp
550            | Strategy::CurrentToBest1Exp => {
551                // Exponential crossover
552                let randn = self.rng.gen_range(0..self.ndim);
553                let mut i = randn;
554                loop {
555                    trial[i] = mutant[i];
556                    i = (i + 1) % self.ndim;
557                    if i == randn || self.rng.gen_range(0.0..1.0) >= self.options.recombination {
558                        break;
559                    }
560                }
561            }
562        }
563
564        // Ensure all trial elements are within bounds after crossover
565        for i in 0..self.ndim {
566            trial[i] = self.ensure_bounds(i, trial[i]);
567        }
568
569        trial
570    }
571
572    /// Run one generation of the algorithm
573    fn evolve(&mut self) -> bool {
574        let popsize = self.population.nrows();
575        let mut converged = true;
576
577        if self.options.parallel.is_some() {
578            // First, generate all mutants and trials
579            let mut trials_and_indices: Vec<(Array1<f64>, usize)> = Vec::with_capacity(popsize);
580            for idx in 0..popsize {
581                let mutant = self.create_mutant(idx);
582                let trial = self.create_trial(idx, &mutant);
583                trials_and_indices.push((trial, idx));
584            }
585
586            // Extract just the trials for batch evaluation
587            let trials: Vec<Array1<f64>> = trials_and_indices
588                .iter()
589                .map(|(trial_, _)| trial_.clone())
590                .collect();
591
592            // Extract the parallel options for evaluation
593            let parallel_opts = self.options.parallel.as_ref().unwrap();
594
595            // Evaluate all trials in parallel
596            let trial_energies = parallel_evaluate_batch(&self.func, &trials, parallel_opts);
597            self.nfev += popsize;
598
599            // Process results
600            for ((trial, idx), trial_energy) in
601                trials_and_indices.into_iter().zip(trial_energies.iter())
602            {
603                if *trial_energy < self.energies[idx] && self.options.updating == "immediate" {
604                    for i in 0..self.ndim {
605                        self.population[[idx, i]] = trial[i];
606                    }
607                    self.energies[idx] = *trial_energy;
608
609                    if *trial_energy < self.best_energy {
610                        self.best_energy = *trial_energy;
611                        self.best_idx = idx;
612                    }
613                }
614
615                // Check convergence
616                let diff = (self.energies[idx] - self.best_energy).abs();
617                if diff > self.options.tol + self.options.atol {
618                    converged = false;
619                }
620            }
621        } else {
622            // Sequential evolution
623            for idx in 0..popsize {
624                let mutant = self.create_mutant(idx);
625                let trial = self.create_trial(idx, &mutant);
626
627                let trial_energy = (self.func)(&trial.view());
628                self.nfev += 1;
629
630                if trial_energy < self.energies[idx] && self.options.updating == "immediate" {
631                    for i in 0..self.ndim {
632                        self.population[[idx, i]] = trial[i];
633                    }
634                    self.energies[idx] = trial_energy;
635
636                    if trial_energy < self.best_energy {
637                        self.best_energy = trial_energy;
638                        self.best_idx = idx;
639                    }
640                }
641
642                // Check convergence
643                let diff = (self.energies[idx] - self.best_energy).abs();
644                if diff > self.options.tol + self.options.atol {
645                    converged = false;
646                }
647            }
648        }
649
650        converged
651    }
652
653    /// Run the differential evolution algorithm
654    pub fn run(&mut self) -> OptimizeResult<f64> {
655        let mut converged = false;
656        let mut nit = 0;
657
658        for _ in 0..self.options.maxiter {
659            converged = self.evolve();
660            nit += 1;
661
662            if converged {
663                break;
664            }
665        }
666
667        let mut result = OptimizeResult {
668            x: self.population.row(self.best_idx).to_owned(),
669            fun: self.best_energy,
670            nfev: self.nfev,
671            func_evals: self.nfev,
672            nit,
673            success: converged,
674            message: if converged {
675                "Optimization converged successfully"
676            } else {
677                "Maximum number of iterations reached"
678            }
679            .to_string(),
680            ..Default::default()
681        };
682
683        // Polish the result with local optimization if requested
684        if self.options.polish {
685            let bounds_vec: Vec<(f64, f64)> = self.bounds.clone();
686            let local_result = minimize(
687                |x| (self.func)(x),
688                &result.x.to_vec(),
689                Method::LBFGS,
690                Some(Options {
691                    bounds: Some(
692                        UnconstrainedBounds::from_vecs(
693                            bounds_vec.iter().map(|b| Some(b.0)).collect(),
694                            bounds_vec.iter().map(|b| Some(b.1)).collect(),
695                        )
696                        .unwrap(),
697                    ),
698                    ..Default::default()
699                }),
700            )
701            .unwrap();
702            if local_result.success && local_result.fun < result.fun {
703                // Ensure polished result respects bounds
704                let mut polished_x = local_result.x;
705                for (i, &(lb, ub)) in self.bounds.iter().enumerate() {
706                    polished_x[i] = polished_x[i].max(lb).min(ub);
707                }
708
709                // Only accept if still better after bounds enforcement
710                let polished_fun = (self.func)(&polished_x.view());
711                if polished_fun < result.fun {
712                    result.x = polished_x;
713                    result.fun = polished_fun;
714                    result.nfev += local_result.nfev + 1; // +1 for our re-evaluation
715                    result.func_evals = result.nfev;
716                }
717            }
718        }
719
720        result
721    }
722}
723
724/// Perform global optimization using differential evolution
725#[allow(dead_code)]
726pub fn differential_evolution<F>(
727    func: F,
728    bounds: Bounds,
729    options: Option<DifferentialEvolutionOptions>,
730    strategy: Option<&str>,
731) -> Result<OptimizeResult<f64>, OptimizeError>
732where
733    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
734{
735    let options = options.unwrap_or_default();
736    let strategy = strategy.unwrap_or("best1bin");
737
738    let mut solver = DifferentialEvolution::new(func, bounds, options, strategy);
739    Ok(solver.run())
740}