1#![doc = include_str!("../README.md")]
2#![doc = include_str!("../REFERENCES.md")]
3#![allow(missing_docs)]
4use std::fmt;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use ndarray::{Array1, Array2};
9use rand::rngs::StdRng;
10use rand::{Rng, SeedableRng};
11use std::time::Instant;
12
13pub mod stack_linear_penalty;
14
15pub mod apply_integrality;
16pub mod apply_wls;
17
18pub mod distinct_indices;
19pub mod init_latin_hypercube;
20pub mod init_random;
21
22pub mod mutant_adaptive;
23pub mod mutant_best1;
24pub mod mutant_best2;
25pub mod mutant_current_to_best1;
26pub mod mutant_rand1;
27pub mod mutant_rand2;
28pub mod mutant_rand_to_best1;
29
30pub mod crossover_binomial;
31pub mod crossover_exponential;
32
33pub mod differential_evolution;
34pub mod function_registry;
35pub mod impl_helpers;
36pub mod metadata;
37pub mod parallel_eval;
38pub mod recorder;
39pub mod run_recorded;
40pub use differential_evolution::differential_evolution;
41pub use parallel_eval::ParallelConfig;
42pub use recorder::{OptimizationRecord, OptimizationRecorder};
43pub use run_recorded::run_recorded_differential_evolution;
44
45pub type ScalarConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
48pub type VectorConstraintFn = Arc<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
50pub type PenaltyTuple = (ScalarConstraintFn, f64);
52pub type CallbackFn = Box<dyn FnMut(&DEIntermediate) -> CallbackAction>;
54
55pub(crate) fn argmin(v: &Array1<f64>) -> (usize, f64) {
56 let mut best_i = 0usize;
57 let mut best_v = v[0];
58 for (i, &val) in v.iter().enumerate() {
59 if val < best_v {
60 best_v = val;
61 best_i = i;
62 }
63 }
64 (best_i, best_v)
65}
66
67#[derive(Debug, Clone, Copy)]
69pub enum Strategy {
70 Best1Bin,
71 Best1Exp,
72 Rand1Bin,
73 Rand1Exp,
74 Rand2Bin,
75 Rand2Exp,
76 CurrentToBest1Bin,
77 CurrentToBest1Exp,
78 Best2Bin,
79 Best2Exp,
80 RandToBest1Bin,
81 RandToBest1Exp,
82 AdaptiveBin,
85 AdaptiveExp,
87}
88
89impl FromStr for Strategy {
90 type Err = String;
91 fn from_str(s: &str) -> Result<Self, Self::Err> {
92 let t = s.to_lowercase();
93 match t.as_str() {
94 "best1bin" | "best1" => Ok(Strategy::Best1Bin),
95 "best1exp" => Ok(Strategy::Best1Exp),
96 "rand1bin" | "rand1" => Ok(Strategy::Rand1Bin),
97 "rand1exp" => Ok(Strategy::Rand1Exp),
98 "rand2bin" | "rand2" => Ok(Strategy::Rand2Bin),
99 "rand2exp" => Ok(Strategy::Rand2Exp),
100 "currenttobest1bin" | "current-to-best1bin" | "current_to_best1bin" => {
101 Ok(Strategy::CurrentToBest1Bin)
102 }
103 "currenttobest1exp" | "current-to-best1exp" | "current_to_best1exp" => {
104 Ok(Strategy::CurrentToBest1Exp)
105 }
106 "best2bin" | "best2" => Ok(Strategy::Best2Bin),
107 "best2exp" => Ok(Strategy::Best2Exp),
108 "randtobest1bin" | "rand-to-best1bin" | "rand_to_best1bin" => {
109 Ok(Strategy::RandToBest1Bin)
110 }
111 "randtobest1exp" | "rand-to-best1exp" | "rand_to_best1exp" => {
112 Ok(Strategy::RandToBest1Exp)
113 }
114 "adaptivebin" | "adaptive-bin" | "adaptive_bin" | "adaptive" => {
115 Ok(Strategy::AdaptiveBin)
116 }
117 "adaptiveexp" | "adaptive-exp" | "adaptive_exp" => Ok(Strategy::AdaptiveExp),
118 _ => Err(format!("unknown strategy: {}", s)),
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, Default)]
125pub enum Crossover {
126 #[default]
128 Binomial,
129 Exponential,
131}
132
133#[derive(Debug, Clone, Copy)]
135pub enum Mutation {
136 Factor(f64),
138 Range { min: f64, max: f64 },
140 Adaptive { initial_f: f64 },
142}
143
144impl Default for Mutation {
145 fn default() -> Self {
146 let _ = Mutation::Factor(0.8);
147 Mutation::Range { min: 0.0, max: 2.0 }
148 }
149}
150
151impl Mutation {
152 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
153 match *self {
154 Mutation::Factor(f) => f,
155 Mutation::Range { min, max } => rng.random_range(min..max),
156 Mutation::Adaptive { initial_f } => initial_f, }
158 }
159
160 #[allow(dead_code)]
162 fn sample_cauchy<R: Rng + ?Sized>(&self, f_m: f64, _scale: f64, rng: &mut R) -> f64 {
163 let perturbation = (rng.random::<f64>() - 0.5) * 0.2; (f_m + perturbation).clamp(0.0, 2.0) }
167}
168
169#[derive(Debug, Clone, Copy, Default)]
171pub enum Init {
172 #[default]
173 LatinHypercube,
174 Random,
175}
176
177#[derive(Debug, Clone, Copy, Default)]
179pub enum Updating {
180 #[default]
181 Deferred,
182}
183
184#[derive(Debug, Clone)]
186pub struct LinearPenalty {
187 pub a: Array2<f64>,
188 pub lb: Array1<f64>,
189 pub ub: Array1<f64>,
190 pub weight: f64,
191}
192
193#[derive(Debug, Clone)]
195pub struct LinearConstraintHelper {
196 pub a: Array2<f64>,
197 pub lb: Array1<f64>,
198 pub ub: Array1<f64>,
199}
200
201impl LinearConstraintHelper {
202 pub fn apply_to(&self, cfg: &mut DEConfig, weight: f64) {
204 use stack_linear_penalty::stack_linear_penalty;
205
206 let new_lp = LinearPenalty {
207 a: self.a.clone(),
208 lb: self.lb.clone(),
209 ub: self.ub.clone(),
210 weight,
211 };
212 match &mut cfg.linear_penalty {
213 Some(existing) => stack_linear_penalty(existing, &new_lp),
214 None => cfg.linear_penalty = Some(new_lp),
215 }
216 }
217}
218
219#[derive(Clone)]
221pub struct NonlinearConstraintHelper {
222 pub fun: VectorConstraintFn,
223 pub lb: Array1<f64>,
224 pub ub: Array1<f64>,
225}
226
227impl NonlinearConstraintHelper {
228 pub fn apply_to(&self, cfg: &mut DEConfig, weight_ineq: f64, weight_eq: f64) {
232 let f = self.fun.clone();
233 let lb = self.lb.clone();
234 let ub = self.ub.clone();
235 let m = lb.len().min(ub.len());
236 for i in 0..m {
237 let l = lb[i];
238 let u = ub[i];
239 if (u - l).abs() < 1e-18 {
240 let fi = f.clone();
241 cfg.penalty_eq.push((
242 Arc::new(move |x: &Array1<f64>| {
243 let y = (fi)(x);
244 y[i] - l
245 }),
246 weight_eq,
247 ));
248 } else {
249 let fi_u = f.clone();
250 cfg.penalty_ineq.push((
251 Arc::new(move |x: &Array1<f64>| {
252 let y = (fi_u)(x);
253 y[i] - u
254 }),
255 weight_ineq,
256 ));
257 let fi_l = f.clone();
258 cfg.penalty_ineq.push((
259 Arc::new(move |x: &Array1<f64>| {
260 let y = (fi_l)(x);
261 l - y[i]
262 }),
263 weight_ineq,
264 ));
265 }
266 }
267 }
268}
269
270#[derive(Debug, Clone)]
272struct AdaptiveState {
273 f_m: f64,
275 cr_m: f64,
277 successful_f: Vec<f64>,
279 successful_cr: Vec<f64>,
281 current_w: f64,
283}
284
285impl AdaptiveState {
286 fn new(config: &AdaptiveConfig) -> Self {
287 Self {
288 f_m: config.f_m,
289 cr_m: config.cr_m,
290 successful_f: Vec::new(),
291 successful_cr: Vec::new(),
292 current_w: config.w_max, }
294 }
295
296 fn update(&mut self, config: &AdaptiveConfig, iter: usize, max_iter: usize) {
298 let iter_ratio = iter as f64 / max_iter as f64;
300 self.current_w = config.w_max - (config.w_max - config.w_min) * iter_ratio;
301
302 if !self.successful_f.is_empty() {
304 let mean_f = self.compute_lehmer_mean(&self.successful_f);
305 self.f_m = (1.0 - config.w_f) * self.f_m + config.w_f * mean_f;
306 self.f_m = self.f_m.clamp(0.2, 1.2);
307 }
308
309 if !self.successful_cr.is_empty() {
311 let mean_cr = self.compute_arithmetic_mean(&self.successful_cr);
312 self.cr_m = (1.0 - config.w_cr) * self.cr_m + config.w_cr * mean_cr;
313 self.cr_m = self.cr_m.clamp(0.1, 0.9);
314 }
315
316 self.successful_f.clear();
318 self.successful_cr.clear();
319 }
320
321 fn compute_lehmer_mean(&self, values: &[f64]) -> f64 {
323 if values.is_empty() {
324 return 0.5; }
326
327 let sum_sq: f64 = values.iter().map(|&x| x * x).sum();
328 let sum: f64 = values.iter().sum();
329
330 if sum > 0.0 {
331 sum_sq / sum
332 } else {
333 0.5 }
335 }
336
337 fn compute_arithmetic_mean(&self, values: &[f64]) -> f64 {
339 if values.is_empty() {
340 return 0.5; }
342 values.iter().sum::<f64>() / values.len() as f64
343 }
344
345 fn record_success(&mut self, f_val: f64, cr_val: f64) {
347 self.successful_f.push(f_val);
348 self.successful_cr.push(cr_val);
349 }
350
351 fn sample_f<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
353 let u1: f64 = rng.random();
355 let u2: f64 = rng.random();
356
357 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
359 let sample = self.f_m + 0.05 * normal; sample.clamp(0.3, 1.0)
363 }
364
365 fn sample_cr<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
367 let u1: f64 = rng.random();
369 let u2: f64 = rng.random();
370
371 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
373 let sample = self.cr_m + 0.05 * normal; sample.clamp(0.1, 0.9)
376 }
377}
378
379#[derive(Debug, Clone)]
381pub struct AdaptiveConfig {
382 pub adaptive_mutation: bool,
384 pub wls_enabled: bool,
386 pub w_max: f64,
388 pub w_min: f64,
390 pub w_f: f64,
392 pub w_cr: f64,
394 pub f_m: f64,
396 pub cr_m: f64,
398 pub wls_prob: f64,
400 pub wls_scale: f64,
402}
403
404impl Default for AdaptiveConfig {
405 fn default() -> Self {
406 Self {
407 adaptive_mutation: false,
408 wls_enabled: false,
409 w_max: 0.9,
410 w_min: 0.1,
411 w_f: 0.9,
412 w_cr: 0.9,
413 f_m: 0.5,
414 cr_m: 0.6,
415 wls_prob: 0.1,
416 wls_scale: 0.1,
417 }
418 }
419}
420
421#[derive(Debug, Clone)]
423pub struct PolishConfig {
424 pub enabled: bool,
425 pub algo: String, pub maxeval: usize, }
428
429pub struct DEConfig {
431 pub maxiter: usize,
432 pub popsize: usize, pub tol: f64,
434 pub atol: f64,
435 pub mutation: Mutation,
436 pub recombination: f64, pub strategy: Strategy,
438 pub crossover: Crossover,
439 pub init: Init,
440 pub updating: Updating,
441 pub seed: Option<u64>,
442 pub integrality: Option<Vec<bool>>,
444 pub x0: Option<Array1<f64>>,
446 pub disp: bool,
448 pub callback: Option<CallbackFn>,
450 pub penalty_ineq: Vec<PenaltyTuple>,
452 pub penalty_eq: Vec<PenaltyTuple>,
454 pub linear_penalty: Option<LinearPenalty>,
456 pub polish: Option<PolishConfig>,
458 pub adaptive: AdaptiveConfig,
460 pub parallel: parallel_eval::ParallelConfig,
462}
463
464impl Default for DEConfig {
465 fn default() -> Self {
466 Self {
467 maxiter: 1000,
468 popsize: 15,
469 tol: 1e-2,
470 atol: 0.0,
471 mutation: Mutation::default(),
472 recombination: 0.7,
473 strategy: Strategy::Best1Bin,
474 crossover: Crossover::default(),
475 init: Init::default(),
476 updating: Updating::default(),
477 seed: None,
478 integrality: None,
479 x0: None,
480 disp: false,
481 callback: None,
482 penalty_ineq: Vec::new(),
483 penalty_eq: Vec::new(),
484 linear_penalty: None,
485 polish: None,
486 adaptive: AdaptiveConfig::default(),
487 parallel: parallel_eval::ParallelConfig::default(),
488 }
489 }
490}
491
492pub struct DEConfigBuilder {
494 cfg: DEConfig,
495}
496impl Default for DEConfigBuilder {
497 fn default() -> Self {
498 Self::new()
499 }
500}
501
502impl DEConfigBuilder {
503 pub fn new() -> Self {
504 Self {
505 cfg: DEConfig::default(),
506 }
507 }
508 pub fn maxiter(mut self, v: usize) -> Self {
509 self.cfg.maxiter = v;
510 self
511 }
512 pub fn popsize(mut self, v: usize) -> Self {
513 self.cfg.popsize = v;
514 self
515 }
516 pub fn tol(mut self, v: f64) -> Self {
517 self.cfg.tol = v;
518 self
519 }
520 pub fn atol(mut self, v: f64) -> Self {
521 self.cfg.atol = v;
522 self
523 }
524 pub fn mutation(mut self, v: Mutation) -> Self {
525 self.cfg.mutation = v;
526 self
527 }
528 pub fn recombination(mut self, v: f64) -> Self {
529 self.cfg.recombination = v;
530 self
531 }
532 pub fn strategy(mut self, v: Strategy) -> Self {
533 self.cfg.strategy = v;
534 self
535 }
536 pub fn crossover(mut self, v: Crossover) -> Self {
537 self.cfg.crossover = v;
538 self
539 }
540 pub fn init(mut self, v: Init) -> Self {
541 self.cfg.init = v;
542 self
543 }
544 pub fn seed(mut self, v: u64) -> Self {
545 self.cfg.seed = Some(v);
546 self
547 }
548 pub fn integrality(mut self, v: Vec<bool>) -> Self {
549 self.cfg.integrality = Some(v);
550 self
551 }
552 pub fn x0(mut self, v: Array1<f64>) -> Self {
553 self.cfg.x0 = Some(v);
554 self
555 }
556 pub fn disp(mut self, v: bool) -> Self {
557 self.cfg.disp = v;
558 self
559 }
560 pub fn callback(mut self, cb: Box<dyn FnMut(&DEIntermediate) -> CallbackAction>) -> Self {
561 self.cfg.callback = Some(cb);
562 self
563 }
564 pub fn add_penalty_ineq<FN>(mut self, f: FN, w: f64) -> Self
565 where
566 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
567 {
568 self.cfg.penalty_ineq.push((Arc::new(f), w));
569 self
570 }
571 pub fn add_penalty_eq<FN>(mut self, f: FN, w: f64) -> Self
572 where
573 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
574 {
575 self.cfg.penalty_eq.push((Arc::new(f), w));
576 self
577 }
578 pub fn linear_penalty(mut self, lp: LinearPenalty) -> Self {
579 self.cfg.linear_penalty = Some(lp);
580 self
581 }
582 pub fn polish(mut self, pol: PolishConfig) -> Self {
583 self.cfg.polish = Some(pol);
584 self
585 }
586 pub fn adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
587 self.cfg.adaptive = adaptive;
588 self
589 }
590 pub fn enable_adaptive_mutation(mut self, enable: bool) -> Self {
591 self.cfg.adaptive.adaptive_mutation = enable;
592 self
593 }
594 pub fn enable_wls(mut self, enable: bool) -> Self {
595 self.cfg.adaptive.wls_enabled = enable;
596 self
597 }
598 pub fn adaptive_weights(mut self, w_max: f64, w_min: f64) -> Self {
599 self.cfg.adaptive.w_max = w_max;
600 self.cfg.adaptive.w_min = w_min;
601 self
602 }
603 pub fn parallel(mut self, parallel: parallel_eval::ParallelConfig) -> Self {
604 self.cfg.parallel = parallel;
605 self
606 }
607 pub fn enable_parallel(mut self, enable: bool) -> Self {
608 self.cfg.parallel.enabled = enable;
609 self
610 }
611 pub fn parallel_threads(mut self, num_threads: usize) -> Self {
612 self.cfg.parallel.num_threads = Some(num_threads);
613 self
614 }
615 pub fn build(self) -> DEConfig {
616 self.cfg
617 }
618}
619
620#[derive(Clone)]
622pub struct DEReport {
623 pub x: Array1<f64>,
624 pub fun: f64,
625 pub success: bool,
626 pub message: String,
627 pub nit: usize,
628 pub nfev: usize,
629 pub population: Array2<f64>,
630 pub population_energies: Array1<f64>,
631}
632
633impl fmt::Debug for DEReport {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 f.debug_struct("DEReport")
636 .field("x", &format!("len={}", self.x.len()))
637 .field("fun", &self.fun)
638 .field("success", &self.success)
639 .field("message", &self.message)
640 .field("nit", &self.nit)
641 .field("nfev", &self.nfev)
642 .field(
643 "population",
644 &format!("{}x{}", self.population.nrows(), self.population.ncols()),
645 )
646 .field(
647 "population_energies",
648 &format!("len={}", self.population_energies.len()),
649 )
650 .finish()
651 }
652}
653
654pub struct DEIntermediate {
656 pub x: Array1<f64>,
657 pub fun: f64,
658 pub convergence: f64, pub iter: usize,
660}
661
662pub enum CallbackAction {
664 Continue,
665 Stop,
666}
667
668pub struct DifferentialEvolution<'a, F>
670where
671 F: Fn(&Array1<f64>) -> f64 + Sync,
672{
673 func: &'a F,
674 lower: Array1<f64>,
675 upper: Array1<f64>,
676 config: DEConfig,
677}
678
679impl<'a, F> DifferentialEvolution<'a, F>
680where
681 F: Fn(&Array1<f64>) -> f64 + Sync,
682{
683 pub fn new(func: &'a F, lower: Array1<f64>, upper: Array1<f64>) -> Self {
685 assert_eq!(lower.len(), upper.len(), "lower/upper size mismatch");
686 Self {
687 func,
688 lower,
689 upper,
690 config: DEConfig::default(),
691 }
692 }
693
694 pub fn config_mut(&mut self) -> &mut DEConfig {
696 &mut self.config
697 }
698
699 pub fn solve(&mut self) -> DEReport {
701 use apply_integrality::apply_integrality;
702 use apply_wls::apply_wls;
703 use crossover_binomial::binomial_crossover;
704 use crossover_exponential::exponential_crossover;
705 use init_latin_hypercube::init_latin_hypercube;
706 use init_random::init_random;
707 use mutant_adaptive::mutant_adaptive;
708 use mutant_best1::mutant_best1;
709 use mutant_best2::mutant_best2;
710 use mutant_current_to_best1::mutant_current_to_best1;
711 use mutant_rand_to_best1::mutant_rand_to_best1;
712 use mutant_rand1::mutant_rand1;
713 use mutant_rand2::mutant_rand2;
714 use parallel_eval::evaluate_trials_parallel;
715 use std::sync::Arc;
716
717 let n = self.lower.len();
718
719 let mut is_free: Vec<bool> = Vec::with_capacity(n);
721 for i in 0..n {
722 is_free.push((self.upper[i] - self.lower[i]).abs() > 0.0);
723 }
724 let n_free = is_free.iter().filter(|&&b| b).count();
725 let _n_equal = n - n_free;
726 if n_free == 0 {
727 let x_fixed = self.lower.clone();
729 let mut x_eval = x_fixed.clone();
730 if let Some(mask) = &self.config.integrality {
731 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
732 }
733 let f = (self.func)(&x_eval);
734 return DEReport {
735 x: x_eval,
736 fun: f,
737 success: true,
738 message: "All variables fixed by bounds".into(),
739 nit: 0,
740 nfev: 1,
741 population: Array2::zeros((1, n)),
742 population_energies: Array1::from(vec![f]),
743 };
744 }
745
746 let npop = self.config.popsize * n_free;
747 let _bounds_span = &self.upper - &self.lower;
748
749 if self.config.disp {
750 eprintln!(
751 "DE Init: {} dimensions ({} free), population={}, maxiter={}",
752 n, n_free, npop, self.config.maxiter
753 );
754 eprintln!(
755 " Strategy: {:?}, Mutation: {:?}, Crossover: CR={:.3}",
756 self.config.strategy, self.config.mutation, self.config.recombination
757 );
758 eprintln!(
759 " Tolerances: tol={:.2e}, atol={:.2e}",
760 self.config.tol, self.config.atol
761 );
762 }
763
764 let timing_enabled = std::env::var("AUTOEQ_DE_TIMING")
766 .map(|v| v != "0")
767 .unwrap_or(false);
768
769 if let Some(n) = self.config.parallel.num_threads {
771 let _ = rayon::ThreadPoolBuilder::new()
773 .num_threads(n)
774 .build_global();
775 }
776
777 let mut rng: StdRng = match self.config.seed {
779 Some(s) => StdRng::seed_from_u64(s),
780 None => {
781 let mut thread_rng = rand::rng();
782 StdRng::from_rng(&mut thread_rng)
783 }
784 };
785
786 let mut pop = match self.config.init {
788 Init::LatinHypercube => {
789 if self.config.disp {
790 eprintln!(" Using Latin Hypercube initialization");
791 }
792 init_latin_hypercube(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
793 }
794 Init::Random => {
795 if self.config.disp {
796 eprintln!(" Using Random initialization");
797 }
798 init_random(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
799 }
800 };
801
802 let mut nfev: usize = 0;
804 if self.config.disp {
805 eprintln!(" Evaluating initial population of {} individuals...", npop);
806 }
807
808 let mut eval_pop = pop.clone();
810 let t_integrality0 = Instant::now();
811 if let Some(mask) = &self.config.integrality {
812 for i in 0..npop {
813 let mut row = eval_pop.row_mut(i);
814 let mut x_eval = row.to_owned();
815 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
816 row.assign(&x_eval);
817 }
818 }
819 let t_integrality = t_integrality0.elapsed();
820
821 let func_ref = self.func;
823 let penalty_ineq_vec: Vec<PenaltyTuple> = self
824 .config
825 .penalty_ineq
826 .iter()
827 .map(|(f, w)| (f.clone(), *w))
828 .collect();
829 let penalty_eq_vec: Vec<PenaltyTuple> = self
830 .config
831 .penalty_eq
832 .iter()
833 .map(|(f, w)| (f.clone(), *w))
834 .collect();
835 let linear_penalty = self.config.linear_penalty.clone();
836
837 let energy_fn = Arc::new(move |x: &Array1<f64>| -> f64 {
838 let base = (func_ref)(x);
839 let mut p = 0.0;
840 for (f, w) in &penalty_ineq_vec {
841 let v = f(x);
842 let viol = v.max(0.0);
843 p += w * viol * viol;
844 }
845 for (h, w) in &penalty_eq_vec {
846 let v = h(x);
847 p += w * v * v;
848 }
849 if let Some(ref lp) = linear_penalty {
850 let ax = lp.a.dot(&x.view());
851 for i in 0..ax.len() {
852 let v = ax[i];
853 let lo = lp.lb[i];
854 let hi = lp.ub[i];
855 if v < lo {
856 let d = lo - v;
857 p += lp.weight * d * d;
858 }
859 if v > hi {
860 let d = v - hi;
861 p += lp.weight * d * d;
862 }
863 }
864 }
865 base + p
866 });
867
868 let t_eval0 = Instant::now();
869 let mut energies = parallel_eval::evaluate_population_parallel(
870 &eval_pop,
871 energy_fn,
872 &self.config.parallel,
873 );
874 let t_eval_init = t_eval0.elapsed();
875 nfev += npop;
876 if timing_enabled {
877 eprintln!(
878 "TIMING init: integrality={:.3} ms, eval={:.3} ms",
879 t_integrality.as_secs_f64() * 1e3,
880 t_eval_init.as_secs_f64() * 1e3
881 );
882 }
883
884 let pop_mean = energies.mean().unwrap_or(0.0);
886 let pop_std = energies.std(0.0);
887 if self.config.disp {
888 eprintln!(
889 " Initial population: mean={:.6e}, std={:.6e}",
890 pop_mean, pop_std
891 );
892 }
893
894 if let Some(x0) = &self.config.x0 {
896 let mut x0c = x0.clone();
897 for i in 0..x0c.len() {
899 x0c[i] = x0c[i].clamp(self.lower[i], self.upper[i]);
900 }
901 if let Some(mask) = &self.config.integrality {
902 apply_integrality(&mut x0c, mask, &self.lower, &self.upper);
903 }
904 let f0 = self.energy(&x0c);
905 nfev += 1;
906 let (best_idx, _best_f) = argmin(&energies);
908 pop.row_mut(best_idx).assign(&x0c.view());
909 energies[best_idx] = f0;
910 }
911
912 let (mut best_idx, mut best_f) = argmin(&energies);
913 let mut best_x = pop.row(best_idx).to_owned();
914
915 if self.config.disp {
916 eprintln!(
917 " Initial best: fitness={:.6e} at index {}",
918 best_f, best_idx
919 );
920 let param_summary: Vec<String> = (0..best_x.len() / 3)
921 .map(|i| {
922 let freq = 10f64.powf(best_x[i * 3]);
923 let q = best_x[i * 3 + 1];
924 let gain = best_x[i * 3 + 2];
925 format!("f{:.0}Hz/Q{:.2}/G{:.2}dB", freq, q, gain)
926 })
927 .collect();
928 eprintln!(" Initial best params: [{}]", param_summary.join(", "));
929 }
930
931 if self.config.disp {
932 eprintln!("DE iter {:4} best_f={:.6e}", 0, best_f);
933 }
934
935 let mut adaptive_state = if matches!(
937 self.config.strategy,
938 Strategy::AdaptiveBin | Strategy::AdaptiveExp
939 ) || self.config.adaptive.adaptive_mutation
940 {
941 Some(AdaptiveState::new(&self.config.adaptive))
942 } else {
943 None
944 };
945
946 let mut success = false;
948 let mut message = String::new();
949 let mut nit = 0;
950 let mut accepted_trials;
951 let mut improvement_count;
952
953 let mut t_build_tot = std::time::Duration::ZERO;
954 let mut t_eval_tot = std::time::Duration::ZERO;
955 let mut t_select_tot = std::time::Duration::ZERO;
956 let mut t_iter_tot = std::time::Duration::ZERO;
957
958 for iter in 1..=self.config.maxiter {
959 nit = iter;
960 accepted_trials = 0;
961 improvement_count = 0;
962
963 let iter_start = Instant::now();
964
965 let sorted_indices = if matches!(
967 self.config.strategy,
968 Strategy::AdaptiveBin | Strategy::AdaptiveExp
969 ) {
970 let mut indices: Vec<usize> = (0..npop).collect();
971 indices.sort_by(|&a, &b| {
972 energies[a]
973 .partial_cmp(&energies[b])
974 .unwrap_or(std::cmp::Ordering::Equal)
975 });
976 indices
977 } else {
978 Vec::new() };
980
981 let t_build0 = Instant::now();
983
984 use rayon::prelude::*;
986 let trial_data: Vec<(Array1<f64>, f64, f64)> = (0..npop)
987 .into_par_iter()
988 .map(|i| {
989 let mut local_rng: StdRng = if let Some(base_seed) = self.config.seed {
991 StdRng::seed_from_u64(
992 base_seed
993 .wrapping_add((iter as u64) << 32)
994 .wrapping_add(i as u64),
995 )
996 } else {
997 let mut thread_rng = rand::rng();
999 StdRng::from_rng(&mut thread_rng)
1000 };
1001
1002 let (f, cr) = if let Some(ref adaptive) = adaptive_state {
1004 let adaptive_f = adaptive.sample_f(&mut local_rng);
1006 let adaptive_cr = adaptive.sample_cr(&mut local_rng);
1007 (adaptive_f, adaptive_cr)
1008 } else {
1009 (
1011 self.config.mutation.sample(&mut local_rng),
1012 self.config.recombination,
1013 )
1014 };
1015
1016 let (mutant, cross) = match self.config.strategy {
1018 Strategy::Best1Bin => (
1019 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1020 Crossover::Binomial,
1021 ),
1022 Strategy::Best1Exp => (
1023 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1024 Crossover::Exponential,
1025 ),
1026 Strategy::Rand1Bin => (
1027 mutant_rand1(i, &pop, f, &mut local_rng),
1028 Crossover::Binomial,
1029 ),
1030 Strategy::Rand1Exp => (
1031 mutant_rand1(i, &pop, f, &mut local_rng),
1032 Crossover::Exponential,
1033 ),
1034 Strategy::Rand2Bin => (
1035 mutant_rand2(i, &pop, f, &mut local_rng),
1036 Crossover::Binomial,
1037 ),
1038 Strategy::Rand2Exp => (
1039 mutant_rand2(i, &pop, f, &mut local_rng),
1040 Crossover::Exponential,
1041 ),
1042 Strategy::CurrentToBest1Bin => (
1043 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1044 Crossover::Binomial,
1045 ),
1046 Strategy::CurrentToBest1Exp => (
1047 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1048 Crossover::Exponential,
1049 ),
1050 Strategy::Best2Bin => (
1051 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1052 Crossover::Binomial,
1053 ),
1054 Strategy::Best2Exp => (
1055 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1056 Crossover::Exponential,
1057 ),
1058 Strategy::RandToBest1Bin => (
1059 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1060 Crossover::Binomial,
1061 ),
1062 Strategy::RandToBest1Exp => (
1063 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1064 Crossover::Exponential,
1065 ),
1066 Strategy::AdaptiveBin => {
1067 if let Some(ref adaptive) = adaptive_state {
1068 (
1069 mutant_adaptive(
1070 i,
1071 &pop,
1072 &sorted_indices,
1073 adaptive.current_w,
1074 f,
1075 &mut local_rng,
1076 ),
1077 Crossover::Binomial,
1078 )
1079 } else {
1080 (
1082 mutant_rand1(i, &pop, f, &mut local_rng),
1083 Crossover::Binomial,
1084 )
1085 }
1086 }
1087 Strategy::AdaptiveExp => {
1088 if let Some(ref adaptive) = adaptive_state {
1089 (
1090 mutant_adaptive(
1091 i,
1092 &pop,
1093 &sorted_indices,
1094 adaptive.current_w,
1095 f,
1096 &mut local_rng,
1097 ),
1098 Crossover::Exponential,
1099 )
1100 } else {
1101 (
1103 mutant_rand1(i, &pop, f, &mut local_rng),
1104 Crossover::Exponential,
1105 )
1106 }
1107 }
1108 };
1109
1110 let crossover = cross;
1112 let trial = match crossover {
1113 Crossover::Binomial => {
1114 binomial_crossover(&pop.row(i).to_owned(), &mutant, cr, &mut local_rng)
1115 }
1116 Crossover::Exponential => exponential_crossover(
1117 &pop.row(i).to_owned(),
1118 &mutant,
1119 cr,
1120 &mut local_rng,
1121 ),
1122 };
1123
1124 let wls_trial = if self.config.adaptive.wls_enabled
1126 && local_rng.random::<f64>() < self.config.adaptive.wls_prob
1127 {
1128 apply_wls(
1129 &trial,
1130 &self.lower,
1131 &self.upper,
1132 self.config.adaptive.wls_scale,
1133 &mut local_rng,
1134 )
1135 } else {
1136 trial.clone()
1137 };
1138
1139 let mut trial_clipped = wls_trial;
1141 for j in 0..trial_clipped.len() {
1142 trial_clipped[j] = trial_clipped[j].clamp(self.lower[j], self.upper[j]);
1143 }
1144
1145 if let Some(mask) = &self.config.integrality {
1147 apply_integrality(&mut trial_clipped, mask, &self.lower, &self.upper);
1148 }
1149
1150 (trial_clipped, f, cr)
1152 })
1153 .collect();
1154
1155 let mut trials = Vec::with_capacity(npop);
1157 let mut trial_params = Vec::with_capacity(npop);
1158 for (trial, f, cr) in trial_data {
1159 trials.push(trial);
1160 trial_params.push((f, cr));
1161 }
1162 let func_ref = self.func;
1164 let penalty_ineq_vec: Vec<PenaltyTuple> = self
1165 .config
1166 .penalty_ineq
1167 .iter()
1168 .map(|(f, w)| (f.clone(), *w))
1169 .collect();
1170 let penalty_eq_vec: Vec<PenaltyTuple> = self
1171 .config
1172 .penalty_eq
1173 .iter()
1174 .map(|(f, w)| (f.clone(), *w))
1175 .collect();
1176 let linear_penalty = self.config.linear_penalty.clone();
1177
1178 let energy_fn_loop = Arc::new(move |x: &Array1<f64>| -> f64 {
1179 let base = (func_ref)(x);
1180 let mut p = 0.0;
1181 for (f, w) in &penalty_ineq_vec {
1182 let v = f(x);
1183 let viol = v.max(0.0);
1184 p += w * viol * viol;
1185 }
1186 for (h, w) in &penalty_eq_vec {
1187 let v = h(x);
1188 p += w * v * v;
1189 }
1190 if let Some(ref lp) = linear_penalty {
1191 let ax = lp.a.dot(&x.view());
1192 for i in 0..ax.len() {
1193 let v = ax[i];
1194 let lo = lp.lb[i];
1195 let hi = lp.ub[i];
1196 if v < lo {
1197 let d = lo - v;
1198 p += lp.weight * d * d;
1199 }
1200 if v > hi {
1201 let d = v - hi;
1202 p += lp.weight * d * d;
1203 }
1204 }
1205 }
1206 base + p
1207 });
1208
1209 let t_build = t_build0.elapsed();
1210 let t_eval0 = Instant::now();
1211 let trial_energies =
1212 evaluate_trials_parallel(trials.clone(), energy_fn_loop, &self.config.parallel);
1213 let t_eval = t_eval0.elapsed();
1214 nfev += npop;
1215
1216 let t_select0 = Instant::now();
1217 for (i, (trial, trial_energy)) in
1219 trials.into_iter().zip(trial_energies.iter()).enumerate()
1220 {
1221 let (f, cr) = trial_params[i];
1222
1223 if *trial_energy <= energies[i] {
1225 pop.row_mut(i).assign(&trial.view());
1226 energies[i] = *trial_energy;
1227 accepted_trials += 1;
1228
1229 if let Some(ref mut adaptive) = adaptive_state {
1231 adaptive.record_success(f, cr);
1232 }
1233
1234 if *trial_energy < best_f {
1236 improvement_count += 1;
1237 }
1238 }
1239 }
1240 let t_select = t_select0.elapsed();
1241
1242 t_build_tot += t_build;
1243 t_eval_tot += t_eval;
1244 t_select_tot += t_select;
1245 let iter_dur = iter_start.elapsed();
1246 t_iter_tot += iter_dur;
1247
1248 if timing_enabled && (iter <= 5 || iter % 10 == 0) {
1249 eprintln!(
1250 "TIMING iter {:4}: build={:.3} ms, eval={:.3} ms, select={:.3} ms, total={:.3} ms",
1251 iter,
1252 t_build.as_secs_f64() * 1e3,
1253 t_eval.as_secs_f64() * 1e3,
1254 t_select.as_secs_f64() * 1e3,
1255 iter_dur.as_secs_f64() * 1e3,
1256 );
1257 }
1258
1259 if let Some(ref mut adaptive) = adaptive_state {
1261 adaptive.update(&self.config.adaptive, iter, self.config.maxiter);
1262 }
1263
1264 let (new_best_idx, new_best_f) = argmin(&energies);
1266 if new_best_f < best_f {
1267 best_idx = new_best_idx;
1268 best_f = new_best_f;
1269 best_x = pop.row(best_idx).to_owned();
1270 }
1271
1272 let pop_mean = energies.mean().unwrap_or(0.0);
1274 let pop_std = energies.std(0.0);
1275 let convergence_threshold = self.config.atol + self.config.tol * pop_mean.abs();
1276
1277 if self.config.disp {
1278 eprintln!(
1279 "DE iter {:4} best_f={:.6e} std={:.3e} accepted={}/{}, improved={}",
1280 iter, best_f, pop_std, accepted_trials, npop, improvement_count
1281 );
1282 }
1283
1284 if let Some(ref mut cb) = self.config.callback {
1286 let intermediate = DEIntermediate {
1287 x: best_x.clone(),
1288 fun: best_f,
1289 convergence: pop_std,
1290 iter,
1291 };
1292 match cb(&intermediate) {
1293 CallbackAction::Stop => {
1294 success = true;
1295 message = "Optimization stopped by callback".to_string();
1296 break;
1297 }
1298 CallbackAction::Continue => {}
1299 }
1300 }
1301
1302 if pop_std <= convergence_threshold {
1303 success = true;
1304 message = format!(
1305 "Converged: std(pop_f)={:.3e} <= threshold={:.3e}",
1306 pop_std, convergence_threshold
1307 );
1308 break;
1309 }
1310 }
1311
1312 if !success {
1313 message = format!("Maximum iterations reached: {}", self.config.maxiter);
1314 }
1315
1316 if self.config.disp {
1317 eprintln!("DE finished: {}", message);
1318 }
1319
1320 let (final_x, final_f, polish_nfev) = if let Some(ref polish_cfg) = self.config.polish {
1322 if polish_cfg.enabled {
1323 self.polish(&best_x)
1324 } else {
1325 (best_x.clone(), best_f, 0)
1326 }
1327 } else {
1328 (best_x.clone(), best_f, 0)
1329 };
1330
1331 if timing_enabled {
1332 eprintln!(
1333 "TIMING total: build={:.3} s, eval={:.3} s, select={:.3} s, iter_total={:.3} s",
1334 t_build_tot.as_secs_f64(),
1335 t_eval_tot.as_secs_f64(),
1336 t_select_tot.as_secs_f64(),
1337 t_iter_tot.as_secs_f64()
1338 );
1339 }
1340
1341 self.finish_report(
1342 pop,
1343 energies,
1344 final_x,
1345 final_f,
1346 success,
1347 message,
1348 nit,
1349 nfev + polish_nfev,
1350 )
1351 }
1352}
1353
1354#[cfg(test)]
1355mod strategy_tests {
1356 use super::*;
1357
1358 #[test]
1359 fn test_parse_strategy_variants() {
1360 assert!(matches!(
1361 "best1exp".parse::<Strategy>().unwrap(),
1362 Strategy::Best1Exp
1363 ));
1364 assert!(matches!(
1365 "rand1bin".parse::<Strategy>().unwrap(),
1366 Strategy::Rand1Bin
1367 ));
1368 assert!(matches!(
1369 "randtobest1exp".parse::<Strategy>().unwrap(),
1370 Strategy::RandToBest1Exp
1371 ));
1372 }
1373}