1use 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
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 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.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 solver.validate_bounds();
228 solver.init_population();
229 solver
230 }
231
232 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 fn init_population(&mut self) {
259 let popsize = self.population.nrows();
260
261 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 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 if self.options.parallel.is_some() {
278 let candidates: Vec<Array1<f64>> = (0..popsize)
280 .map(|i| self.population.row(i).to_owned())
281 .collect();
282
283 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 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 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 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 fn init_latinhypercube(&mut self) {
327 let popsize = self.population.nrows();
328
329 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 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 let base = primes[j % primes.len()];
358 let halton_value = self.halton_number(i + 1, base);
359
360 let (lb, ub) = self.bounds[j];
362 self.population[[i, j]] = lb + halton_value * (ub - lb);
363 }
364 }
365 }
366
367 fn init_sobol(&mut self) {
369 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 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 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 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 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 val
414 } else if val < lb {
415 let excess = lb - val;
417 let range = ub - lb;
418 if excess <= range {
419 lb + excess
420 } else {
421 self.rng.gen_range(lb..ub)
423 }
424 } else {
425 let excess = val - ub;
427 let range = ub - lb;
428 if excess <= range {
429 ub - excess
430 } else {
431 self.rng.gen_range(lb..ub)
433 }
434 }
435 }
436
437 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 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 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 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 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 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 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 for i in 0..self.ndim {
519 mutant[i] = self.ensure_bounds(i, mutant[i]);
520 }
521
522 mutant
523 }
524
525 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 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 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 for i in 0..self.ndim {
564 trial[i] = self.ensure_bounds(i, trial[i]);
565 }
566
567 trial
568 }
569
570 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 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 let trials: Vec<Array1<f64>> = trials_and_indices
586 .iter()
587 .map(|(trial_, _)| trial_.clone())
588 .collect();
589
590 let parallel_opts = self.options.parallel.as_ref().unwrap();
592
593 let trial_energies = parallel_evaluate_batch(&self.func, &trials, parallel_opts);
595 self.nfev += popsize;
596
597 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 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 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 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 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 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 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 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; result.func_evals = result.nfev;
714 }
715 }
716 }
717
718 result
719 }
720}
721
722#[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}