1use 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
19struct 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 for d in 0..dimension {
34 let mut dirs = Vec::new();
35 if d == 0 {
36 for i in 0..32 {
38 dirs.push(1u32 << (31 - i));
39 }
40 } else {
41 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 point.push(x as f64 / (1u64 << 32) as f64);
79 }
80
81 point
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct DifferentialEvolutionOptions {
88 pub maxiter: usize,
90 pub popsize: usize,
92 pub tol: f64,
94 pub mutation: (f64, f64),
96 pub recombination: f64,
98 pub polish: bool,
100 pub init: String,
102 pub atol: f64,
104 pub updating: String,
106 pub seed: Option<u64>,
108 pub x0: Option<Array1<f64>>,
110 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#[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
166pub type Bounds = Vec<(f64, f64)>;
168
169pub 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 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 solver.validate_bounds();
230 solver.init_population();
231 solver
232 }
233
234 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 fn init_population(&mut self) {
261 let popsize = self.population.nrows();
262
263 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 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 if self.options.parallel.is_some() {
280 let candidates: Vec<Array1<f64>> = (0..popsize)
282 .map(|i| self.population.row(i).to_owned())
283 .collect();
284
285 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 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 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 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 fn init_latinhypercube(&mut self) {
329 let popsize = self.population.nrows();
330
331 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 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 let base = primes[j % primes.len()];
360 let halton_value = self.halton_number(i + 1, base);
361
362 let (lb, ub) = self.bounds[j];
364 self.population[[i, j]] = lb + halton_value * (ub - lb);
365 }
366 }
367 }
368
369 fn init_sobol(&mut self) {
371 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 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 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 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 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 val
416 } else if val < lb {
417 let excess = lb - val;
419 let range = ub - lb;
420 if excess <= range {
421 lb + excess
422 } else {
423 self.rng.gen_range(lb..ub)
425 }
426 } else {
427 let excess = val - ub;
429 let range = ub - lb;
430 if excess <= range {
431 ub - excess
432 } else {
433 self.rng.gen_range(lb..ub)
435 }
436 }
437 }
438
439 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 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 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 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 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 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 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 for i in 0..self.ndim {
521 mutant[i] = self.ensure_bounds(i, mutant[i]);
522 }
523
524 mutant
525 }
526
527 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 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 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 for i in 0..self.ndim {
566 trial[i] = self.ensure_bounds(i, trial[i]);
567 }
568
569 trial
570 }
571
572 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 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 let trials: Vec<Array1<f64>> = trials_and_indices
588 .iter()
589 .map(|(trial_, _)| trial_.clone())
590 .collect();
591
592 let parallel_opts = self.options.parallel.as_ref().unwrap();
594
595 let trial_energies = parallel_evaluate_batch(&self.func, &trials, parallel_opts);
597 self.nfev += popsize;
598
599 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 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 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 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 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 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 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 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; result.func_evals = result.nfev;
716 }
717 }
718 }
719
720 result
721 }
722}
723
724#[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}