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::*;
15use rand::rngs::StdRng;
16
17#[derive(Debug, Clone)]
19pub struct DifferentialEvolutionOptions {
20 pub maxiter: usize,
22 pub popsize: usize,
24 pub tol: f64,
26 pub mutation: (f64, f64),
28 pub recombination: f64,
30 pub polish: bool,
32 pub init: String,
34 pub atol: f64,
36 pub updating: String,
38 pub seed: Option<u64>,
40 pub x0: Option<Array1<f64>>,
42 pub parallel: Option<ParallelOptions>,
44}
45
46impl Default for DifferentialEvolutionOptions {
47 fn default() -> Self {
48 Self {
49 maxiter: 1000,
50 popsize: 15,
51 tol: 0.01,
52 mutation: (0.5, 1.0),
53 recombination: 0.7,
54 polish: true,
55 init: "latinhypercube".to_string(),
56 atol: 0.0,
57 updating: "immediate".to_string(),
58 seed: None,
59 x0: None,
60 parallel: None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum Strategy {
68 Best1Bin,
69 Best1Exp,
70 Rand1Bin,
71 Rand1Exp,
72 Best2Bin,
73 Best2Exp,
74 Rand2Bin,
75 Rand2Exp,
76 CurrentToBest1Bin,
77 CurrentToBest1Exp,
78}
79
80impl Strategy {
81 fn from_str(s: &str) -> Option<Self> {
82 match s {
83 "best1bin" => Some(Strategy::Best1Bin),
84 "best1exp" => Some(Strategy::Best1Exp),
85 "rand1bin" => Some(Strategy::Rand1Bin),
86 "rand1exp" => Some(Strategy::Rand1Exp),
87 "best2bin" => Some(Strategy::Best2Bin),
88 "best2exp" => Some(Strategy::Best2Exp),
89 "rand2bin" => Some(Strategy::Rand2Bin),
90 "rand2exp" => Some(Strategy::Rand2Exp),
91 "currenttobest1bin" => Some(Strategy::CurrentToBest1Bin),
92 "currenttobest1exp" => Some(Strategy::CurrentToBest1Exp),
93 _ => None,
94 }
95 }
96}
97
98pub type Bounds = Vec<(f64, f64)>;
100
101pub struct DifferentialEvolution<F>
103where
104 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
105{
106 func: F,
107 bounds: Bounds,
108 options: DifferentialEvolutionOptions,
109 strategy: Strategy,
110 ndim: usize,
111 population: Array2<f64>,
112 energies: Array1<f64>,
113 best_energy: f64,
114 best_idx: usize,
115 rng: StdRng,
116 nfev: usize,
117}
118
119use ndarray::Array2;
120
121impl<F> DifferentialEvolution<F>
122where
123 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
124{
125 pub fn new(
127 func: F,
128 bounds: Bounds,
129 options: DifferentialEvolutionOptions,
130 strategy: &str,
131 ) -> Self {
132 let ndim = bounds.len();
133 let popsize = if options.popsize < ndim {
134 options.popsize * ndim
135 } else {
136 options.popsize
137 };
138
139 let seed = options.seed.unwrap_or_else(rand::random);
140 let rng = StdRng::seed_from_u64(seed);
141
142 let strategy_enum = Strategy::from_str(strategy).unwrap_or(Strategy::Best1Bin);
143
144 let mut solver = Self {
145 func,
146 bounds,
147 options,
148 strategy: strategy_enum,
149 ndim,
150 population: Array2::zeros((popsize, ndim)),
151 energies: Array1::zeros(popsize),
152 best_energy: f64::INFINITY,
153 best_idx: 0,
154 rng,
155 nfev: 0,
156 };
157
158 solver.init_population();
159 solver
160 }
161
162 fn init_population(&mut self) {
164 let popsize = self.population.nrows();
165
166 match self.options.init.as_str() {
168 "latinhypercube" => self.init_latinhypercube(),
169 "halton" => self.init_halton(),
170 "sobol" => self.init_sobol(),
171 _ => self.init_random(),
172 }
173
174 if let Some(ref x0) = self.options.x0 {
176 for (i, &val) in x0.iter().enumerate() {
177 self.population[[0, i]] = val;
178 }
179 }
180
181 if self.options.parallel.is_some() {
183 let candidates: Vec<Array1<f64>> = (0..popsize)
185 .map(|i| self.population.row(i).to_owned())
186 .collect();
187
188 let parallel_opts = self.options.parallel.as_ref().unwrap();
190
191 let energies = parallel_evaluate_batch(&self.func, &candidates, parallel_opts);
192 self.energies = Array1::from_vec(energies);
193 self.nfev += popsize;
194
195 for i in 0..popsize {
197 if self.energies[i] < self.best_energy {
198 self.best_energy = self.energies[i];
199 self.best_idx = i;
200 }
201 }
202 } else {
203 for i in 0..popsize {
205 let candidate = self.population.row(i);
206 self.energies[i] = (self.func)(&candidate);
207 self.nfev += 1;
208
209 if self.energies[i] < self.best_energy {
210 self.best_energy = self.energies[i];
211 self.best_idx = i;
212 }
213 }
214 }
215 }
216
217 fn init_random(&mut self) {
219 let popsize = self.population.nrows();
220
221 for i in 0..popsize {
222 for j in 0..self.ndim {
223 let (lb, ub) = self.bounds[j];
224 let uniform = Uniform::new(lb, ub).unwrap();
225 self.population[[i, j]] = self.rng.sample(uniform);
226 }
227 }
228 }
229
230 fn init_latinhypercube(&mut self) {
232 let popsize = self.population.nrows();
233
234 for j in 0..self.ndim {
236 let (lb, ub) = self.bounds[j];
237 let segment_size = (ub - lb) / popsize as f64;
238
239 let mut segments: Vec<usize> = (0..popsize).collect();
240 segments.shuffle(&mut self.rng);
241
242 for (i, &seg) in segments.iter().enumerate() {
243 let segment_lb = lb + seg as f64 * segment_size;
244 let segment_ub = segment_lb + segment_size;
245 let uniform = Uniform::new(segment_lb, segment_ub).unwrap();
246 self.population[[i, j]] = self.rng.sample(uniform);
247 }
248 }
249 }
250
251 fn init_halton(&mut self) {
253 self.init_random(); }
256
257 fn init_sobol(&mut self) {
259 self.init_random(); }
262
263 fn ensure_bounds(&self, idx: usize, val: f64) -> f64 {
265 let (lb, ub) = self.bounds[idx];
266 val.max(lb).min(ub)
267 }
268
269 fn create_mutant(&mut self, candidate_idx: usize) -> Array1<f64> {
271 let popsize = self.population.nrows();
272 let mut mutant = Array1::zeros(self.ndim);
273
274 let mut indices: Vec<usize> = Vec::with_capacity(5);
276 while indices.len() < 5 {
277 let idx = self.rng.random_range(0..popsize);
278 if idx != candidate_idx && !indices.contains(&idx) {
279 indices.push(idx);
280 }
281 }
282
283 let mutation_factor = if self.options.mutation.0 == self.options.mutation.1 {
284 self.options.mutation.0
285 } else {
286 self.rng
287 .random_range(self.options.mutation.0..self.options.mutation.1)
288 };
289
290 match self.strategy {
291 Strategy::Best1Bin | Strategy::Best1Exp => {
292 let best = self.population.row(self.best_idx);
294 let r1 = self.population.row(indices[0]);
295 let r2 = self.population.row(indices[1]);
296 for i in 0..self.ndim {
297 mutant[i] = best[i] + mutation_factor * (r1[i] - r2[i]);
298 mutant[i] = self.ensure_bounds(i, mutant[i]);
299 }
300 }
301 Strategy::Rand1Bin | Strategy::Rand1Exp => {
302 let r0 = self.population.row(indices[0]);
304 let r1 = self.population.row(indices[1]);
305 let r2 = self.population.row(indices[2]);
306 for i in 0..self.ndim {
307 mutant[i] = r0[i] + mutation_factor * (r1[i] - r2[i]);
308 mutant[i] = self.ensure_bounds(i, mutant[i]);
309 }
310 }
311 Strategy::Best2Bin | Strategy::Best2Exp => {
312 let best = self.population.row(self.best_idx);
314 let r1 = self.population.row(indices[0]);
315 let r2 = self.population.row(indices[1]);
316 let r3 = self.population.row(indices[2]);
317 let r4 = self.population.row(indices[3]);
318 for i in 0..self.ndim {
319 mutant[i] = best[i]
320 + mutation_factor * (r1[i] - r2[i])
321 + mutation_factor * (r3[i] - r4[i]);
322 mutant[i] = self.ensure_bounds(i, mutant[i]);
323 }
324 }
325 Strategy::Rand2Bin | Strategy::Rand2Exp => {
326 let r0 = self.population.row(indices[0]);
328 let r1 = self.population.row(indices[1]);
329 let r2 = self.population.row(indices[2]);
330 let r3 = self.population.row(indices[3]);
331 let r4 = self.population.row(indices[4]);
332 for i in 0..self.ndim {
333 mutant[i] = r0[i]
334 + mutation_factor * (r1[i] - r2[i])
335 + mutation_factor * (r3[i] - r4[i]);
336 mutant[i] = self.ensure_bounds(i, mutant[i]);
337 }
338 }
339 Strategy::CurrentToBest1Bin | Strategy::CurrentToBest1Exp => {
340 let current = self.population.row(candidate_idx);
342 let best = self.population.row(self.best_idx);
343 let r1 = self.population.row(indices[0]);
344 let r2 = self.population.row(indices[1]);
345 for i in 0..self.ndim {
346 mutant[i] = current[i]
347 + mutation_factor * (best[i] - current[i])
348 + mutation_factor * (r1[i] - r2[i]);
349 mutant[i] = self.ensure_bounds(i, mutant[i]);
350 }
351 }
352 }
353
354 mutant
355 }
356
357 fn create_trial(&mut self, candidate_idx: usize, mutant: &Array1<f64>) -> Array1<f64> {
359 let candidate = self.population.row(candidate_idx).to_owned();
360 let mut trial = candidate.clone();
361
362 match self.strategy {
363 Strategy::Best1Bin
364 | Strategy::Rand1Bin
365 | Strategy::Best2Bin
366 | Strategy::Rand2Bin
367 | Strategy::CurrentToBest1Bin => {
368 let randn = self.rng.random_range(0..self.ndim);
370 for i in 0..self.ndim {
371 if i == randn || self.rng.random::<f64>() < self.options.recombination {
372 trial[i] = mutant[i];
373 }
374 }
375 }
376 Strategy::Best1Exp
377 | Strategy::Rand1Exp
378 | Strategy::Best2Exp
379 | Strategy::Rand2Exp
380 | Strategy::CurrentToBest1Exp => {
381 let randn = self.rng.random_range(0..self.ndim);
383 let mut i = randn;
384 loop {
385 trial[i] = mutant[i];
386 i = (i + 1) % self.ndim;
387 if i == randn || self.rng.random::<f64>() >= self.options.recombination {
388 break;
389 }
390 }
391 }
392 }
393
394 trial
395 }
396
397 fn evolve(&mut self) -> bool {
399 let popsize = self.population.nrows();
400 let mut converged = true;
401
402 if self.options.parallel.is_some() {
403 let mut trials_and_indices: Vec<(Array1<f64>, usize)> = Vec::with_capacity(popsize);
405 for idx in 0..popsize {
406 let mutant = self.create_mutant(idx);
407 let trial = self.create_trial(idx, &mutant);
408 trials_and_indices.push((trial, idx));
409 }
410
411 let trials: Vec<Array1<f64>> = trials_and_indices
413 .iter()
414 .map(|(trial, _)| trial.clone())
415 .collect();
416
417 let parallel_opts = self.options.parallel.as_ref().unwrap();
419
420 let trial_energies = parallel_evaluate_batch(&self.func, &trials, parallel_opts);
422 self.nfev += popsize;
423
424 for ((trial, idx), trial_energy) in
426 trials_and_indices.into_iter().zip(trial_energies.iter())
427 {
428 if *trial_energy < self.energies[idx] && self.options.updating == "immediate" {
429 for i in 0..self.ndim {
430 self.population[[idx, i]] = trial[i];
431 }
432 self.energies[idx] = *trial_energy;
433
434 if *trial_energy < self.best_energy {
435 self.best_energy = *trial_energy;
436 self.best_idx = idx;
437 }
438 }
439
440 let diff = (self.energies[idx] - self.best_energy).abs();
442 if diff > self.options.tol + self.options.atol {
443 converged = false;
444 }
445 }
446 } else {
447 for idx in 0..popsize {
449 let mutant = self.create_mutant(idx);
450 let trial = self.create_trial(idx, &mutant);
451
452 let trial_energy = (self.func)(&trial.view());
453 self.nfev += 1;
454
455 if trial_energy < self.energies[idx] && self.options.updating == "immediate" {
456 for i in 0..self.ndim {
457 self.population[[idx, i]] = trial[i];
458 }
459 self.energies[idx] = trial_energy;
460
461 if trial_energy < self.best_energy {
462 self.best_energy = trial_energy;
463 self.best_idx = idx;
464 }
465 }
466
467 let diff = (self.energies[idx] - self.best_energy).abs();
469 if diff > self.options.tol + self.options.atol {
470 converged = false;
471 }
472 }
473 }
474
475 converged
476 }
477
478 pub fn run(&mut self) -> OptimizeResult<f64> {
480 let mut converged = false;
481 let mut nit = 0;
482
483 for _ in 0..self.options.maxiter {
484 converged = self.evolve();
485 nit += 1;
486
487 if converged {
488 break;
489 }
490 }
491
492 let mut result = OptimizeResult {
493 x: self.population.row(self.best_idx).to_owned(),
494 fun: self.best_energy,
495 nfev: self.nfev,
496 func_evals: self.nfev,
497 nit,
498 iterations: nit,
499 success: converged,
500 message: if converged {
501 "Optimization converged successfully"
502 } else {
503 "Maximum number of iterations reached"
504 }
505 .to_string(),
506 ..Default::default()
507 };
508
509 if self.options.polish {
511 let bounds_vec: Vec<(f64, f64)> = self.bounds.clone();
512 let local_result = minimize(
513 |x| (self.func)(x),
514 &result.x.to_vec(),
515 Method::LBFGS,
516 Some(Options {
517 bounds: Some(
518 UnconstrainedBounds::from_vecs(
519 bounds_vec.iter().map(|b| Some(b.0)).collect(),
520 bounds_vec.iter().map(|b| Some(b.1)).collect(),
521 )
522 .unwrap(),
523 ),
524 ..Default::default()
525 }),
526 )
527 .unwrap();
528 if local_result.success && local_result.fun < result.fun {
529 result.x = local_result.x;
530 result.fun = local_result.fun;
531 result.nfev += local_result.nfev;
532 result.func_evals = result.nfev;
533 }
534 }
535
536 result
537 }
538}
539
540pub fn differential_evolution<F>(
542 func: F,
543 bounds: Bounds,
544 options: Option<DifferentialEvolutionOptions>,
545 strategy: Option<&str>,
546) -> Result<OptimizeResult<f64>, OptimizeError>
547where
548 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Sync,
549{
550 let options = options.unwrap_or_default();
551 let strategy = strategy.unwrap_or("best1bin");
552
553 let mut solver = DifferentialEvolution::new(func, bounds, options, strategy);
554 Ok(solver.run())
555}