1use crate::error::OptimizeError;
8use crate::global::qmc::SobolGenerator;
9use crate::parallel::{parallel_evaluate_batch, ParallelOptions};
10use crate::unconstrained::{
11 minimize, Bounds as UnconstrainedBounds, Method, OptimizeResult, Options,
12};
13use scirs2_core::ndarray::{Array1, ArrayView1};
14use scirs2_core::random::prelude::SliceRandom;
15use scirs2_core::random::rngs::StdRng;
16use scirs2_core::random::Random;
17use scirs2_core::random::Uniform;
18use scirs2_core::random::{Rng, RngExt, SeedableRng};
19
20#[derive(Debug, Clone)]
22pub struct DifferentialEvolutionOptions {
23 pub maxiter: usize,
25 pub popsize: usize,
27 pub tol: f64,
29 pub mutation: (f64, f64),
31 pub recombination: f64,
33 pub polish: bool,
35 pub init: String,
37 pub atol: f64,
39 pub updating: String,
41 pub seed: Option<u64>,
43 pub x0: Option<Array1<f64>>,
45 pub parallel: Option<ParallelOptions>,
47}
48
49impl Default for DifferentialEvolutionOptions {
50 fn default() -> Self {
51 Self {
52 maxiter: 1000,
53 popsize: 15,
54 tol: 0.01,
55 mutation: (0.5, 1.0),
56 recombination: 0.7,
57 polish: true,
58 init: "latinhypercube".to_string(),
59 atol: 0.0,
60 updating: "immediate".to_string(),
61 seed: None,
62 x0: None,
63 parallel: None,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
70pub enum Strategy {
71 Best1Bin,
72 Best1Exp,
73 Rand1Bin,
74 Rand1Exp,
75 Best2Bin,
76 Best2Exp,
77 Rand2Bin,
78 Rand2Exp,
79 CurrentToBest1Bin,
80 CurrentToBest1Exp,
81}
82
83impl Strategy {
84 fn from_str(s: &str) -> Option<Self> {
85 match s {
86 "best1bin" => Some(Strategy::Best1Bin),
87 "best1exp" => Some(Strategy::Best1Exp),
88 "rand1bin" => Some(Strategy::Rand1Bin),
89 "rand1exp" => Some(Strategy::Rand1Exp),
90 "best2bin" => Some(Strategy::Best2Bin),
91 "best2exp" => Some(Strategy::Best2Exp),
92 "rand2bin" => Some(Strategy::Rand2Bin),
93 "rand2exp" => Some(Strategy::Rand2Exp),
94 "currenttobest1bin" => Some(Strategy::CurrentToBest1Bin),
95 "currenttobest1exp" => Some(Strategy::CurrentToBest1Exp),
96 _ => None,
97 }
98 }
99}
100
101pub type Bounds = Vec<(f64, f64)>;
103
104pub struct DifferentialEvolution<F>
106where
107 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
108{
109 func: F,
110 bounds: Bounds,
111 options: DifferentialEvolutionOptions,
112 strategy: Strategy,
113 ndim: usize,
114 population: Array2<f64>,
115 energies: Array1<f64>,
116 best_energy: f64,
117 best_idx: usize,
118 rng: Random<StdRng>,
119 nfev: usize,
120}
121
122use scirs2_core::ndarray::Array2;
123
124impl<F> DifferentialEvolution<F>
125where
126 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
127{
128 pub fn new(
130 func: F,
131 bounds: Bounds,
132 options: DifferentialEvolutionOptions,
133 strategy: &str,
134 ) -> Self {
135 let ndim = bounds.len();
136 let popsize = if options.popsize < ndim {
137 options.popsize * ndim
138 } else {
139 options.popsize
140 };
141
142 let seed = options
143 .seed
144 .unwrap_or_else(|| scirs2_core::random::rng().random());
145 let rng = Random::seed(seed);
146
147 let strategy_enum = Strategy::from_str(strategy).unwrap_or(Strategy::Best1Bin);
148
149 let mut solver = Self {
150 func,
151 bounds,
152 options,
153 strategy: strategy_enum,
154 ndim,
155 population: Array2::zeros((popsize, ndim)),
156 energies: Array1::zeros(popsize),
157 best_energy: f64::INFINITY,
158 best_idx: 0,
159 rng,
160 nfev: 0,
161 };
162
163 solver.validate_bounds();
165 solver.init_population();
166 solver
167 }
168
169 fn validate_bounds(&self) {
171 for (i, &(lb, ub)) in self.bounds.iter().enumerate() {
172 if !lb.is_finite() || !ub.is_finite() {
173 panic!(
174 "Bounds must be finite values. Variable {}: bounds = ({}, {})",
175 i, lb, ub
176 );
177 }
178 if lb >= ub {
179 panic!(
180 "Lower bound must be less than upper bound. Variable {}: lb = {}, ub = {}",
181 i, lb, ub
182 );
183 }
184 if (ub - lb) < 1e-12 {
185 panic!(
186 "Bounds range is too small. Variable {}: range = {}",
187 i,
188 ub - lb
189 );
190 }
191 }
192 }
193
194 fn init_population(&mut self) {
196 let popsize = self.population.nrows();
197
198 match self.options.init.as_str() {
200 "latinhypercube" => self.init_latinhypercube(),
201 "halton" => self.init_halton(),
202 "sobol" => self.init_sobol(),
203 _ => self.init_random(),
204 }
205
206 if let Some(x0) = self.options.x0.clone() {
208 for (i, &val) in x0.iter().enumerate() {
209 self.population[[0, i]] = self.ensure_bounds(i, val);
210 }
211 }
212
213 if self.options.parallel.is_some() {
215 let candidates: Vec<Array1<f64>> = (0..popsize)
217 .map(|i| self.population.row(i).to_owned())
218 .collect();
219
220 let parallel_opts = self.options.parallel.as_ref().expect("Operation failed");
222
223 let energies = parallel_evaluate_batch(&self.func, &candidates, parallel_opts);
224 self.energies = Array1::from_vec(energies);
225 self.nfev += popsize;
226
227 for i in 0..popsize {
229 if self.energies[i] < self.best_energy {
230 self.best_energy = self.energies[i];
231 self.best_idx = i;
232 }
233 }
234 } else {
235 for i in 0..popsize {
237 let candidate = self.population.row(i);
238 self.energies[i] = (self.func)(&candidate);
239 self.nfev += 1;
240
241 if self.energies[i] < self.best_energy {
242 self.best_energy = self.energies[i];
243 self.best_idx = i;
244 }
245 }
246 }
247 }
248
249 fn init_random(&mut self) {
251 let popsize = self.population.nrows();
252
253 for i in 0..popsize {
254 for j in 0..self.ndim {
255 let (lb, ub) = self.bounds[j];
256 let uniform = Uniform::new(lb, ub).expect("Operation failed");
257 self.population[[i, j]] = self.rng.sample(uniform);
258 }
259 }
260 }
261
262 fn init_latinhypercube(&mut self) {
264 let popsize = self.population.nrows();
265
266 for j in 0..self.ndim {
268 let (lb, ub) = self.bounds[j];
269 let segment_size = (ub - lb) / popsize as f64;
270
271 let mut segments: Vec<usize> = (0..popsize).collect();
272 segments.shuffle(&mut self.rng);
273
274 for (i, &seg) in segments.iter().enumerate() {
275 let segment_lb = lb + seg as f64 * segment_size;
276 let segment_ub = segment_lb + segment_size;
277 let uniform = Uniform::new(segment_lb, segment_ub).expect("Operation failed");
278 self.population[[i, j]] = self.rng.sample(uniform);
279 }
280 }
281 }
282
283 fn init_halton(&mut self) {
285 let primes: [u64; 25] = [
286 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83,
287 89, 97,
288 ];
289
290 let popsize = self.population.nrows();
291 for i in 0..popsize {
292 for j in 0..self.ndim {
293 let base = primes[j % primes.len()];
295 let halton_value = crate::global::qmc::halton_radical_inverse(i + 1, base);
296
297 let (lb, ub) = self.bounds[j];
299 self.population[[i, j]] = lb + halton_value * (ub - lb);
300 }
301 }
302 }
303
304 fn init_sobol(&mut self) {
309 let mut sobol_gen = SobolGenerator::new(self.ndim);
310
311 let popsize = self.population.nrows();
312 for i in 0..popsize {
313 let sobol_point = sobol_gen.next_point();
314 for j in 0..self.ndim {
315 let (lb, ub) = self.bounds[j];
316 self.population[[i, j]] = lb + sobol_point[j] * (ub - lb);
317 }
318 }
319 }
320
321 fn ensure_bounds(&mut self, idx: usize, val: f64) -> f64 {
323 let (lb, ub) = self.bounds[idx];
324
325 if val >= lb && val <= ub {
326 val
328 } else if val < lb {
329 let excess = lb - val;
331 let range = ub - lb;
332 if excess <= range {
333 lb + excess
334 } else {
335 self.rng.random_range(lb..ub)
337 }
338 } else {
339 let excess = val - ub;
341 let range = ub - lb;
342 if excess <= range {
343 ub - excess
344 } else {
345 self.rng.random_range(lb..ub)
347 }
348 }
349 }
350
351 fn create_mutant(&mut self, candidate_idx: usize) -> Array1<f64> {
353 let popsize = self.population.nrows();
354 let mut mutant = Array1::zeros(self.ndim);
355
356 let mut indices: Vec<usize> = Vec::with_capacity(5);
358 while indices.len() < 5 {
359 let idx = self.rng.random_range(0..popsize);
360 if idx != candidate_idx && !indices.contains(&idx) {
361 indices.push(idx);
362 }
363 }
364
365 let mutation_factor = if self.options.mutation.0 == self.options.mutation.1 {
366 self.options.mutation.0
367 } else {
368 self.rng
369 .random_range(self.options.mutation.0..self.options.mutation.1)
370 };
371
372 match self.strategy {
373 Strategy::Best1Bin | Strategy::Best1Exp => {
374 let best = self.population.row(self.best_idx);
376 let r1 = self.population.row(indices[0]);
377 let r2 = self.population.row(indices[1]);
378 for i in 0..self.ndim {
379 mutant[i] = best[i] + mutation_factor * (r1[i] - r2[i]);
380 }
381 }
382 Strategy::Rand1Bin | Strategy::Rand1Exp => {
383 let r0 = self.population.row(indices[0]);
385 let r1 = self.population.row(indices[1]);
386 let r2 = self.population.row(indices[2]);
387 for i in 0..self.ndim {
388 mutant[i] = r0[i] + mutation_factor * (r1[i] - r2[i]);
389 }
390 }
391 Strategy::Best2Bin | Strategy::Best2Exp => {
392 let best = self.population.row(self.best_idx);
394 let r1 = self.population.row(indices[0]);
395 let r2 = self.population.row(indices[1]);
396 let r3 = self.population.row(indices[2]);
397 let r4 = self.population.row(indices[3]);
398 for i in 0..self.ndim {
399 mutant[i] = best[i]
400 + mutation_factor * (r1[i] - r2[i])
401 + mutation_factor * (r3[i] - r4[i]);
402 }
403 }
404 Strategy::Rand2Bin | Strategy::Rand2Exp => {
405 let r0 = self.population.row(indices[0]);
407 let r1 = self.population.row(indices[1]);
408 let r2 = self.population.row(indices[2]);
409 let r3 = self.population.row(indices[3]);
410 let r4 = self.population.row(indices[4]);
411 for i in 0..self.ndim {
412 mutant[i] = r0[i]
413 + mutation_factor * (r1[i] - r2[i])
414 + mutation_factor * (r3[i] - r4[i]);
415 }
416 }
417 Strategy::CurrentToBest1Bin | Strategy::CurrentToBest1Exp => {
418 let current = self.population.row(candidate_idx);
420 let best = self.population.row(self.best_idx);
421 let r1 = self.population.row(indices[0]);
422 let r2 = self.population.row(indices[1]);
423 for i in 0..self.ndim {
424 mutant[i] = current[i]
425 + mutation_factor * (best[i] - current[i])
426 + mutation_factor * (r1[i] - r2[i]);
427 }
428 }
429 }
430
431 for i in 0..self.ndim {
433 mutant[i] = self.ensure_bounds(i, mutant[i]);
434 }
435
436 mutant
437 }
438
439 fn create_trial(&mut self, candidate_idx: usize, mutant: &Array1<f64>) -> Array1<f64> {
441 let candidate = self.population.row(candidate_idx).to_owned();
442 let mut trial = candidate.clone();
443
444 match self.strategy {
445 Strategy::Best1Bin
446 | Strategy::Rand1Bin
447 | Strategy::Best2Bin
448 | Strategy::Rand2Bin
449 | Strategy::CurrentToBest1Bin => {
450 let randn = self.rng.random_range(0..self.ndim);
452 for i in 0..self.ndim {
453 if i == randn || self.rng.random_range(0.0..1.0) < self.options.recombination {
454 trial[i] = mutant[i];
455 }
456 }
457 }
458 Strategy::Best1Exp
459 | Strategy::Rand1Exp
460 | Strategy::Best2Exp
461 | Strategy::Rand2Exp
462 | Strategy::CurrentToBest1Exp => {
463 let randn = self.rng.random_range(0..self.ndim);
465 let mut i = randn;
466 loop {
467 trial[i] = mutant[i];
468 i = (i + 1) % self.ndim;
469 if i == randn || self.rng.random_range(0.0..1.0) >= self.options.recombination {
470 break;
471 }
472 }
473 }
474 }
475
476 for i in 0..self.ndim {
478 trial[i] = self.ensure_bounds(i, trial[i]);
479 }
480
481 trial
482 }
483
484 fn evolve(&mut self) -> bool {
486 let popsize = self.population.nrows();
487 let mut converged = true;
488
489 if self.options.parallel.is_some() {
490 let mut trials_and_indices: Vec<(Array1<f64>, usize)> = Vec::with_capacity(popsize);
492 for idx in 0..popsize {
493 let mutant = self.create_mutant(idx);
494 let trial = self.create_trial(idx, &mutant);
495 trials_and_indices.push((trial, idx));
496 }
497
498 let trials: Vec<Array1<f64>> = trials_and_indices
500 .iter()
501 .map(|(trial_, _)| trial_.clone())
502 .collect();
503
504 let parallel_opts = self.options.parallel.as_ref().expect("Operation failed");
506
507 let trial_energies = parallel_evaluate_batch(&self.func, &trials, parallel_opts);
509 self.nfev += popsize;
510
511 for ((trial, idx), trial_energy) in
513 trials_and_indices.into_iter().zip(trial_energies.iter())
514 {
515 if *trial_energy < self.energies[idx] && self.options.updating == "immediate" {
516 for i in 0..self.ndim {
517 self.population[[idx, i]] = trial[i];
518 }
519 self.energies[idx] = *trial_energy;
520
521 if *trial_energy < self.best_energy {
522 self.best_energy = *trial_energy;
523 self.best_idx = idx;
524 }
525 }
526
527 let diff = (self.energies[idx] - self.best_energy).abs();
529 if diff > self.options.tol + self.options.atol {
530 converged = false;
531 }
532 }
533 } else {
534 for idx in 0..popsize {
536 let mutant = self.create_mutant(idx);
537 let trial = self.create_trial(idx, &mutant);
538
539 let trial_energy = (self.func)(&trial.view());
540 self.nfev += 1;
541
542 if trial_energy < self.energies[idx] && self.options.updating == "immediate" {
543 for i in 0..self.ndim {
544 self.population[[idx, i]] = trial[i];
545 }
546 self.energies[idx] = trial_energy;
547
548 if trial_energy < self.best_energy {
549 self.best_energy = trial_energy;
550 self.best_idx = idx;
551 }
552 }
553
554 let diff = (self.energies[idx] - self.best_energy).abs();
556 if diff > self.options.tol + self.options.atol {
557 converged = false;
558 }
559 }
560 }
561
562 converged
563 }
564
565 pub fn run(&mut self) -> OptimizeResult<f64> {
567 let mut converged = false;
568 let mut nit = 0;
569
570 for _ in 0..self.options.maxiter {
571 converged = self.evolve();
572 nit += 1;
573
574 if converged {
575 break;
576 }
577 }
578
579 let mut result = OptimizeResult {
580 x: self.population.row(self.best_idx).to_owned(),
581 fun: self.best_energy,
582 nfev: self.nfev,
583 func_evals: self.nfev,
584 nit,
585 success: converged,
586 message: if converged {
587 "Optimization converged successfully"
588 } else {
589 "Maximum number of iterations reached"
590 }
591 .to_string(),
592 ..Default::default()
593 };
594
595 if self.options.polish {
597 let bounds_vec: Vec<(f64, f64)> = self.bounds.clone();
598 let local_result = minimize(
599 |x| (self.func)(x),
600 &result.x.to_vec(),
601 Method::LBFGS,
602 Some(Options {
603 bounds: Some(
604 UnconstrainedBounds::from_vecs(
605 bounds_vec.iter().map(|b| Some(b.0)).collect(),
606 bounds_vec.iter().map(|b| Some(b.1)).collect(),
607 )
608 .expect("Operation failed"),
609 ),
610 ..Default::default()
611 }),
612 )
613 .expect("Operation failed");
614 if local_result.success && local_result.fun < result.fun {
615 let mut polished_x = local_result.x;
617 for (i, &(lb, ub)) in self.bounds.iter().enumerate() {
618 polished_x[i] = polished_x[i].max(lb).min(ub);
619 }
620
621 let polished_fun = (self.func)(&polished_x.view());
623 if polished_fun < result.fun {
624 result.x = polished_x;
625 result.fun = polished_fun;
626 result.nfev += local_result.nfev + 1; result.func_evals = result.nfev;
628 }
629 }
630 }
631
632 result
633 }
634}
635
636#[allow(dead_code)]
649pub fn differential_evolution<F>(
650 func: F,
651 bounds: Bounds,
652 options: Option<DifferentialEvolutionOptions>,
653 strategy: Option<&str>,
654) -> Result<OptimizeResult<f64>, OptimizeError>
655where
656 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
657{
658 if bounds.is_empty() {
659 return Err(OptimizeError::InvalidInput(
660 "Bounds must not be empty".to_string(),
661 ));
662 }
663 for (i, &(lb, ub)) in bounds.iter().enumerate() {
664 if !lb.is_finite() || !ub.is_finite() {
665 return Err(OptimizeError::InvalidInput(format!(
666 "Bounds must be finite for dimension {} ({}, {})",
667 i, lb, ub
668 )));
669 }
670 if lb >= ub {
671 return Err(OptimizeError::InvalidInput(format!(
672 "Lower bound must be less than upper bound for dimension {} ({} >= {})",
673 i, lb, ub
674 )));
675 }
676 }
677
678 let options = options.unwrap_or_default();
679 let strategy = strategy.unwrap_or("best1bin");
680
681 let mut solver = DifferentialEvolution::new(func, bounds, options, strategy);
682 Ok(solver.run())
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688 use approx::assert_abs_diff_eq;
689
690 fn sphere(x: &ArrayView1<f64>) -> f64 {
691 x.iter().map(|xi| xi * xi).sum()
692 }
693
694 fn rastrigin(x: &ArrayView1<f64>) -> f64 {
695 let n = x.len() as f64;
696 10.0 * n
697 + x.iter()
698 .map(|xi| xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos())
699 .sum::<f64>()
700 }
701
702 fn rosenbrock(x: &ArrayView1<f64>) -> f64 {
703 let mut sum = 0.0;
704 for i in 0..x.len() - 1 {
705 sum += 100.0 * (x[i + 1] - x[i].powi(2)).powi(2) + (1.0 - x[i]).powi(2);
706 }
707 sum
708 }
709
710 #[test]
711 fn test_de_sphere_best1bin() {
712 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
713 let mut opts = DifferentialEvolutionOptions::default();
714 opts.seed = Some(42);
715 opts.maxiter = 200;
716 opts.polish = false;
717
718 let result = differential_evolution(sphere, bounds, Some(opts), Some("best1bin"));
719 assert!(result.is_ok());
720 let res = result.expect("should succeed");
721 assert!(res.fun < 0.01, "Sphere min should be ~0, got {}", res.fun);
722 }
723
724 #[test]
725 fn test_de_sphere_rand1bin() {
726 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
727 let mut opts = DifferentialEvolutionOptions::default();
728 opts.seed = Some(42);
729 opts.maxiter = 300;
730 opts.polish = false;
731
732 let result = differential_evolution(sphere, bounds, Some(opts), Some("rand1bin"));
733 assert!(result.is_ok());
734 let res = result.expect("should succeed");
735 assert!(
736 res.fun < 0.1,
737 "Sphere with rand1bin should be near 0, got {}",
738 res.fun
739 );
740 }
741
742 #[test]
743 fn test_de_currenttobest1bin() {
744 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
745 let mut opts = DifferentialEvolutionOptions::default();
746 opts.seed = Some(42);
747 opts.maxiter = 300;
748 opts.polish = false;
749
750 let result = differential_evolution(sphere, bounds, Some(opts), Some("currenttobest1bin"));
751 assert!(result.is_ok());
752 let res = result.expect("should succeed");
753 assert!(
754 res.fun < 0.1,
755 "currenttobest1bin should be near 0, got {}",
756 res.fun
757 );
758 }
759
760 #[test]
761 fn test_de_rastrigin() {
762 let bounds = vec![(-5.12, 5.12), (-5.12, 5.12)];
763 let mut opts = DifferentialEvolutionOptions::default();
764 opts.seed = Some(42);
765 opts.popsize = 30;
766 opts.maxiter = 500;
767 opts.polish = false;
768
769 let result = differential_evolution(rastrigin, bounds, Some(opts), None);
770 assert!(result.is_ok());
771 let res = result.expect("should succeed");
772 assert!(
773 res.fun < 10.0,
774 "DE should find reasonable Rastrigin value, got {}",
775 res.fun
776 );
777 }
778
779 #[test]
780 fn test_de_rosenbrock() {
781 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
782 let mut opts = DifferentialEvolutionOptions::default();
783 opts.seed = Some(42);
784 opts.popsize = 30;
785 opts.maxiter = 500;
786 opts.polish = false;
787
788 let result = differential_evolution(rosenbrock, bounds, Some(opts), None);
789 assert!(result.is_ok());
790 let res = result.expect("should succeed");
791 assert!(
792 res.fun < 5.0,
793 "DE should find reasonable Rosenbrock value, got {}",
794 res.fun
795 );
796 }
797
798 #[test]
799 fn test_de_with_initial_guess() {
800 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
801 let mut opts = DifferentialEvolutionOptions::default();
802 opts.seed = Some(42);
803 opts.maxiter = 100;
804 opts.polish = false;
805 opts.x0 = Some(Array1::from_vec(vec![0.1, 0.1]));
806
807 let result = differential_evolution(sphere, bounds, Some(opts), None);
808 assert!(result.is_ok());
809 let res = result.expect("should succeed");
810 assert!(res.fun < 0.1);
811 }
812
813 #[test]
814 fn test_de_sobol_init_matches_validated_sobol_sequence() {
815 let bounds = vec![(0.0, 10.0), (0.0, 20.0), (-4.0, 4.0)];
827 let mut opts = DifferentialEvolutionOptions::default();
828 opts.seed = Some(1);
829 opts.init = "sobol".to_string();
830
831 let solver = DifferentialEvolution::new(sphere, bounds.clone(), opts, "best1bin");
832
833 for j in 0..bounds.len() {
835 assert_abs_diff_eq!(solver.population[[0, j]], bounds[j].0, epsilon = 1e-9);
836 }
837 for j in 0..bounds.len() {
839 let mid = (bounds[j].0 + bounds[j].1) / 2.0;
840 assert_abs_diff_eq!(solver.population[[1, j]], mid, epsilon = 1e-9);
841 }
842 }
843
844 #[test]
845 fn test_de_init_methods() {
846 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
847
848 for init_method in &["latinhypercube", "halton", "sobol", "random"] {
849 let mut opts = DifferentialEvolutionOptions::default();
850 opts.seed = Some(42);
851 opts.maxiter = 50;
852 opts.polish = false;
853 opts.init = init_method.to_string();
854
855 let result = differential_evolution(sphere, bounds.clone(), Some(opts), None);
856 assert!(
857 result.is_ok(),
858 "Init method {} should not fail",
859 init_method
860 );
861 }
862 }
863
864 #[test]
865 fn test_de_invalid_bounds() {
866 let result = differential_evolution(sphere, vec![], None, None);
868 assert!(result.is_err());
869
870 let bounds = vec![(5.0, 2.0)];
872 let result = differential_evolution(sphere, bounds, None, None);
873 assert!(result.is_err());
874 }
875
876 #[test]
877 fn test_de_convergence() {
878 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
879 let mut opts = DifferentialEvolutionOptions::default();
880 opts.seed = Some(42);
881 opts.maxiter = 500;
882 opts.tol = 1.0; opts.polish = false;
884
885 let result = differential_evolution(sphere, bounds, Some(opts), None);
886 assert!(result.is_ok());
887 let res = result.expect("should succeed");
888 assert!(res.nit > 0);
889 assert!(res.nfev > 0);
890 }
891
892 #[test]
893 fn test_de_options_default() {
894 let opts = DifferentialEvolutionOptions::default();
895 assert_eq!(opts.maxiter, 1000);
896 assert_eq!(opts.popsize, 15);
897 assert!((opts.tol - 0.01).abs() < 1e-12);
898 assert!((opts.recombination - 0.7).abs() < 1e-12);
899 assert!(opts.polish);
900 assert_eq!(opts.init, "latinhypercube");
901 }
902
903 #[test]
904 fn test_de_strategy_from_str() {
905 assert_eq!(Strategy::from_str("best1bin"), Some(Strategy::Best1Bin));
906 assert_eq!(Strategy::from_str("rand1bin"), Some(Strategy::Rand1Bin));
907 assert_eq!(
908 Strategy::from_str("currenttobest1bin"),
909 Some(Strategy::CurrentToBest1Bin)
910 );
911 assert_eq!(Strategy::from_str("best2bin"), Some(Strategy::Best2Bin));
912 assert_eq!(Strategy::from_str("rand2bin"), Some(Strategy::Rand2Bin));
913 assert_eq!(Strategy::from_str("best1exp"), Some(Strategy::Best1Exp));
914 assert_eq!(Strategy::from_str("rand1exp"), Some(Strategy::Rand1Exp));
915 assert_eq!(Strategy::from_str("unknown"), None);
916 }
917
918 #[test]
919 fn test_de_high_dimensional() {
920 let n = 5;
921 let bounds: Vec<(f64, f64)> = vec![(-5.0, 5.0); n];
922 let mut opts = DifferentialEvolutionOptions::default();
923 opts.seed = Some(42);
924 opts.popsize = 30;
925 opts.maxiter = 500;
926 opts.polish = false;
927
928 let result = differential_evolution(sphere, bounds, Some(opts), None);
929 assert!(result.is_ok());
930 let res = result.expect("should succeed");
931 assert!(
932 res.fun < 1.0,
933 "High-dim DE should converge reasonably, got {}",
934 res.fun
935 );
936 }
937}