1#![doc = include_str!("../README.md")]
38#![doc = include_str!("../REFERENCES.md")]
39#![warn(missing_docs)]
40
41pub mod error;
42pub use error::{DEError, Result};
43
44use std::fmt;
45use std::str::FromStr;
46use std::sync::Arc;
47
48use ndarray::{Array1, Array2, Zip};
49use rand::rngs::StdRng;
50use rand::{Rng, SeedableRng};
51use std::time::Instant;
52
53pub mod stack_linear_penalty;
55
56pub mod apply_integrality;
58pub mod apply_wls;
60
61pub mod distinct_indices;
63pub mod init_latin_hypercube;
65pub mod init_random;
67
68pub mod mutant_adaptive;
70pub mod mutant_best1;
72pub mod mutant_best2;
74pub mod mutant_current_to_best1;
76pub mod mutant_rand1;
78pub mod mutant_rand2;
80pub mod mutant_rand_to_best1;
82
83pub mod crossover_binomial;
85pub mod crossover_exponential;
87
88#[cfg(test)]
90mod de_tests;
91pub mod differential_evolution;
93pub mod function_registry;
95pub mod impl_helpers;
97pub mod metadata;
99pub mod parallel_eval;
101pub mod recorder;
103pub mod run_recorded;
105pub use differential_evolution::differential_evolution;
106pub use parallel_eval::ParallelConfig;
107pub use recorder::{OptimizationRecord, OptimizationRecorder};
108pub use run_recorded::run_recorded_differential_evolution;
109
110pub type ScalarConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
113pub type VectorConstraintFn = Arc<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
115pub type PenaltyTuple = (ScalarConstraintFn, f64);
117pub type CallbackFn = Box<dyn FnMut(&DEIntermediate) -> CallbackAction>;
119
120pub(crate) fn argmin(v: &Array1<f64>) -> (usize, f64) {
121 let mut best_i = 0usize;
122 let mut best_v = v[0];
123 for (i, &val) in v.iter().enumerate() {
124 if val < best_v {
125 best_v = val;
126 best_i = i;
127 }
128 }
129 (best_i, best_v)
130}
131
132#[derive(Debug, Clone, Copy)]
139pub enum Strategy {
140 Best1Bin,
142 Best1Exp,
144 Rand1Bin,
146 Rand1Exp,
148 Rand2Bin,
150 Rand2Exp,
152 CurrentToBest1Bin,
154 CurrentToBest1Exp,
156 Best2Bin,
158 Best2Exp,
160 RandToBest1Bin,
162 RandToBest1Exp,
164 AdaptiveBin,
166 AdaptiveExp,
168}
169
170impl FromStr for Strategy {
171 type Err = String;
172 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
173 let t = s.to_lowercase();
174 match t.as_str() {
175 "best1bin" | "best1" => Ok(Strategy::Best1Bin),
176 "best1exp" => Ok(Strategy::Best1Exp),
177 "rand1bin" | "rand1" => Ok(Strategy::Rand1Bin),
178 "rand1exp" => Ok(Strategy::Rand1Exp),
179 "rand2bin" | "rand2" => Ok(Strategy::Rand2Bin),
180 "rand2exp" => Ok(Strategy::Rand2Exp),
181 "currenttobest1bin" | "current-to-best1bin" | "current_to_best1bin" => {
182 Ok(Strategy::CurrentToBest1Bin)
183 }
184 "currenttobest1exp" | "current-to-best1exp" | "current_to_best1exp" => {
185 Ok(Strategy::CurrentToBest1Exp)
186 }
187 "best2bin" | "best2" => Ok(Strategy::Best2Bin),
188 "best2exp" => Ok(Strategy::Best2Exp),
189 "randtobest1bin" | "rand-to-best1bin" | "rand_to_best1bin" => {
190 Ok(Strategy::RandToBest1Bin)
191 }
192 "randtobest1exp" | "rand-to-best1exp" | "rand_to_best1exp" => {
193 Ok(Strategy::RandToBest1Exp)
194 }
195 "adaptivebin" | "adaptive-bin" | "adaptive_bin" | "adaptive" => {
196 Ok(Strategy::AdaptiveBin)
197 }
198 "adaptiveexp" | "adaptive-exp" | "adaptive_exp" => Ok(Strategy::AdaptiveExp),
199 _ => Err(format!("unknown strategy: {}", s)),
200 }
201 }
202}
203
204#[derive(Debug, Clone, Copy, Default)]
206pub enum Crossover {
207 #[default]
209 Binomial,
210 Exponential,
212}
213
214#[derive(Debug, Clone, Copy)]
216pub enum Mutation {
217 Factor(f64),
219 Range {
221 min: f64,
223 max: f64,
225 },
226 Adaptive {
228 initial_f: f64,
230 },
231}
232
233impl Default for Mutation {
234 fn default() -> Self {
235 let _ = Mutation::Factor(0.8);
236 Mutation::Range { min: 0.0, max: 2.0 }
237 }
238}
239
240impl Mutation {
241 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
242 match *self {
243 Mutation::Factor(f) => f,
244 Mutation::Range { min, max } => rng.random_range(min..max),
245 Mutation::Adaptive { initial_f } => initial_f, }
247 }
248
249 #[allow(dead_code)]
251 fn sample_cauchy<R: Rng + ?Sized>(&self, f_m: f64, _scale: f64, rng: &mut R) -> f64 {
252 let perturbation = (rng.random::<f64>() - 0.5) * 0.2; (f_m + perturbation).clamp(0.0, 2.0) }
256}
257
258#[derive(Debug, Clone, Copy, Default)]
260pub enum Init {
261 #[default]
263 LatinHypercube,
264 Random,
266}
267
268#[derive(Debug, Clone, Copy, Default)]
270pub enum Updating {
271 #[default]
273 Deferred,
274}
275
276#[derive(Debug, Clone)]
278pub struct LinearPenalty {
279 pub a: Array2<f64>,
281 pub lb: Array1<f64>,
283 pub ub: Array1<f64>,
285 pub weight: f64,
287}
288
289#[derive(Debug, Clone)]
291pub struct LinearConstraintHelper {
292 pub a: Array2<f64>,
294 pub lb: Array1<f64>,
296 pub ub: Array1<f64>,
298}
299
300impl LinearConstraintHelper {
301 pub fn apply_to(&self, cfg: &mut DEConfig, weight: f64) {
303 use stack_linear_penalty::stack_linear_penalty;
304
305 let new_lp = LinearPenalty {
306 a: self.a.clone(),
307 lb: self.lb.clone(),
308 ub: self.ub.clone(),
309 weight,
310 };
311 match &mut cfg.linear_penalty {
312 Some(existing) => stack_linear_penalty(existing, &new_lp),
313 None => cfg.linear_penalty = Some(new_lp),
314 }
315 }
316}
317
318#[derive(Clone)]
320pub struct NonlinearConstraintHelper {
321 pub fun: VectorConstraintFn,
323 pub lb: Array1<f64>,
325 pub ub: Array1<f64>,
327}
328
329impl NonlinearConstraintHelper {
330 pub fn apply_to(&self, cfg: &mut DEConfig, weight_ineq: f64, weight_eq: f64) {
334 let f = self.fun.clone();
335 let lb = self.lb.clone();
336 let ub = self.ub.clone();
337 let m = lb.len().min(ub.len());
338 for i in 0..m {
339 let l = lb[i];
340 let u = ub[i];
341 let tol = 1e-12 * (l.abs() + u.abs()).max(1.0);
342 if (u - l).abs() <= tol {
343 let fi = f.clone();
344 cfg.penalty_eq.push((
345 Arc::new(move |x: &Array1<f64>| {
346 let y = (fi)(x);
347 y[i] - l
348 }),
349 weight_eq,
350 ));
351 } else {
352 let fi_u = f.clone();
353 cfg.penalty_ineq.push((
354 Arc::new(move |x: &Array1<f64>| {
355 let y = (fi_u)(x);
356 y[i] - u
357 }),
358 weight_ineq,
359 ));
360 let fi_l = f.clone();
361 cfg.penalty_ineq.push((
362 Arc::new(move |x: &Array1<f64>| {
363 let y = (fi_l)(x);
364 l - y[i]
365 }),
366 weight_ineq,
367 ));
368 }
369 }
370 }
371}
372
373#[derive(Debug, Clone)]
375struct AdaptiveState {
376 f_m: f64,
378 cr_m: f64,
380 successful_f: Vec<f64>,
382 successful_cr: Vec<f64>,
384 current_w: f64,
386}
387
388impl AdaptiveState {
389 fn new(config: &AdaptiveConfig) -> Self {
390 Self {
391 f_m: config.f_m,
392 cr_m: config.cr_m,
393 successful_f: Vec::new(),
394 successful_cr: Vec::new(),
395 current_w: config.w_max, }
397 }
398
399 fn update(&mut self, config: &AdaptiveConfig, iter: usize, max_iter: usize) {
401 let iter_ratio = iter as f64 / max_iter as f64;
403 self.current_w = config.w_max - (config.w_max - config.w_min) * iter_ratio;
404
405 if !self.successful_f.is_empty() {
407 let mean_f = self.compute_lehmer_mean(&self.successful_f);
408 self.f_m = (1.0 - config.w_f) * self.f_m + config.w_f * mean_f;
409 self.f_m = self.f_m.clamp(0.2, 1.2);
410 }
411
412 if !self.successful_cr.is_empty() {
414 let mean_cr = self.compute_arithmetic_mean(&self.successful_cr);
415 self.cr_m = (1.0 - config.w_cr) * self.cr_m + config.w_cr * mean_cr;
416 self.cr_m = self.cr_m.clamp(0.1, 0.9);
417 }
418
419 self.successful_f.clear();
421 self.successful_cr.clear();
422 }
423
424 fn compute_lehmer_mean(&self, values: &[f64]) -> f64 {
426 if values.is_empty() {
427 return 0.5; }
429
430 let sum_sq: f64 = values.iter().map(|&x| x * x).sum();
431 let sum: f64 = values.iter().sum();
432
433 if sum > 0.0 {
434 sum_sq / sum
435 } else {
436 0.5 }
438 }
439
440 fn compute_arithmetic_mean(&self, values: &[f64]) -> f64 {
442 if values.is_empty() {
443 return 0.5; }
445 values.iter().sum::<f64>() / values.len() as f64
446 }
447
448 fn record_success(&mut self, f_val: f64, cr_val: f64) {
450 self.successful_f.push(f_val);
451 self.successful_cr.push(cr_val);
452 }
453
454 fn sample_f<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
456 let u1: f64 = rng.random::<f64>().max(1e-15);
457 let u2: f64 = rng.random::<f64>();
458
459 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
460 let sample = self.f_m + 0.05 * normal;
461
462 sample.clamp(0.3, 1.0)
463 }
464
465 fn sample_cr<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
467 let u1: f64 = rng.random::<f64>().max(1e-15);
468 let u2: f64 = rng.random::<f64>();
469
470 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
471 let sample = self.cr_m + 0.05 * normal;
472
473 sample.clamp(0.1, 0.9)
474 }
475}
476
477#[derive(Debug, Clone)]
479pub struct AdaptiveConfig {
480 pub adaptive_mutation: bool,
482 pub wls_enabled: bool,
484 pub w_max: f64,
486 pub w_min: f64,
488 pub w_f: f64,
490 pub w_cr: f64,
492 pub f_m: f64,
494 pub cr_m: f64,
496 pub wls_prob: f64,
498 pub wls_scale: f64,
500}
501
502impl Default for AdaptiveConfig {
503 fn default() -> Self {
504 Self {
505 adaptive_mutation: false,
506 wls_enabled: false,
507 w_max: 0.9,
508 w_min: 0.1,
509 w_f: 0.9,
510 w_cr: 0.9,
511 f_m: 0.5,
512 cr_m: 0.6,
513 wls_prob: 0.1,
514 wls_scale: 0.1,
515 }
516 }
517}
518
519#[derive(Debug, Clone)]
521pub struct PolishConfig {
522 pub enabled: bool,
524 pub algo: String,
526 pub maxeval: usize,
528}
529
530pub struct DEConfig {
536 pub maxiter: usize,
538 pub popsize: usize,
540 pub tol: f64,
542 pub atol: f64,
544 pub mutation: Mutation,
546 pub recombination: f64,
548 pub strategy: Strategy,
550 pub crossover: Crossover,
552 pub init: Init,
554 pub updating: Updating,
556 pub seed: Option<u64>,
558 pub integrality: Option<Vec<bool>>,
560 pub x0: Option<Array1<f64>>,
562 pub disp: bool,
564 pub callback: Option<CallbackFn>,
566 pub penalty_ineq: Vec<PenaltyTuple>,
568 pub penalty_eq: Vec<PenaltyTuple>,
570 pub linear_penalty: Option<LinearPenalty>,
572 pub polish: Option<PolishConfig>,
574 pub adaptive: AdaptiveConfig,
576 pub parallel: parallel_eval::ParallelConfig,
578}
579
580impl Default for DEConfig {
581 fn default() -> Self {
582 Self {
583 maxiter: 1000,
584 popsize: 15,
585 tol: 1e-2,
586 atol: 0.0,
587 mutation: Mutation::default(),
588 recombination: 0.7,
589 strategy: Strategy::Best1Bin,
590 crossover: Crossover::default(),
591 init: Init::default(),
592 updating: Updating::default(),
593 seed: None,
594 integrality: None,
595 x0: None,
596 disp: false,
597 callback: None,
598 penalty_ineq: Vec::new(),
599 penalty_eq: Vec::new(),
600 linear_penalty: None,
601 polish: None,
602 adaptive: AdaptiveConfig::default(),
603 parallel: parallel_eval::ParallelConfig::default(),
604 }
605 }
606}
607
608pub struct DEConfigBuilder {
625 cfg: DEConfig,
626}
627impl Default for DEConfigBuilder {
628 fn default() -> Self {
629 Self::new()
630 }
631}
632
633impl DEConfigBuilder {
634 pub fn new() -> Self {
636 Self {
637 cfg: DEConfig::default(),
638 }
639 }
640 pub fn maxiter(mut self, v: usize) -> Self {
642 self.cfg.maxiter = v;
643 self
644 }
645 pub fn popsize(mut self, v: usize) -> Self {
652 self.cfg.popsize = v;
653 self
654 }
655 pub fn tol(mut self, v: f64) -> Self {
657 self.cfg.tol = v;
658 self
659 }
660 pub fn atol(mut self, v: f64) -> Self {
662 self.cfg.atol = v;
663 self
664 }
665 pub fn mutation(mut self, v: Mutation) -> Self {
667 self.cfg.mutation = v;
668 self
669 }
670 pub fn recombination(mut self, v: f64) -> Self {
672 self.cfg.recombination = v;
673 self
674 }
675 pub fn strategy(mut self, v: Strategy) -> Self {
677 self.cfg.strategy = v;
678 self
679 }
680 pub fn crossover(mut self, v: Crossover) -> Self {
682 self.cfg.crossover = v;
683 self
684 }
685 pub fn init(mut self, v: Init) -> Self {
687 self.cfg.init = v;
688 self
689 }
690 pub fn seed(mut self, v: u64) -> Self {
692 self.cfg.seed = Some(v);
693 self
694 }
695 pub fn integrality(mut self, v: Vec<bool>) -> Self {
697 self.cfg.integrality = Some(v);
698 self
699 }
700 pub fn x0(mut self, v: Array1<f64>) -> Self {
702 self.cfg.x0 = Some(v);
703 self
704 }
705 pub fn disp(mut self, v: bool) -> Self {
707 self.cfg.disp = v;
708 self
709 }
710 pub fn callback(mut self, cb: Box<dyn FnMut(&DEIntermediate) -> CallbackAction>) -> Self {
712 self.cfg.callback = Some(cb);
713 self
714 }
715 pub fn add_penalty_ineq<FN>(mut self, f: FN, w: f64) -> Self
717 where
718 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
719 {
720 self.cfg.penalty_ineq.push((Arc::new(f), w));
721 self
722 }
723 pub fn add_penalty_eq<FN>(mut self, f: FN, w: f64) -> Self
725 where
726 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
727 {
728 self.cfg.penalty_eq.push((Arc::new(f), w));
729 self
730 }
731 pub fn linear_penalty(mut self, lp: LinearPenalty) -> Self {
733 self.cfg.linear_penalty = Some(lp);
734 self
735 }
736 pub fn polish(mut self, pol: PolishConfig) -> Self {
738 self.cfg.polish = Some(pol);
739 self
740 }
741 pub fn adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
743 self.cfg.adaptive = adaptive;
744 self
745 }
746 pub fn enable_adaptive_mutation(mut self, enable: bool) -> Self {
748 self.cfg.adaptive.adaptive_mutation = enable;
749 self
750 }
751 pub fn enable_wls(mut self, enable: bool) -> Self {
753 self.cfg.adaptive.wls_enabled = enable;
754 self
755 }
756 pub fn adaptive_weights(mut self, w_max: f64, w_min: f64) -> Self {
758 self.cfg.adaptive.w_max = w_max;
759 self.cfg.adaptive.w_min = w_min;
760 self
761 }
762 pub fn parallel(mut self, parallel: parallel_eval::ParallelConfig) -> Self {
764 self.cfg.parallel = parallel;
765 self
766 }
767 pub fn enable_parallel(mut self, enable: bool) -> Self {
769 self.cfg.parallel.enabled = enable;
770 self
771 }
772 pub fn parallel_threads(mut self, num_threads: usize) -> Self {
774 self.cfg.parallel.num_threads = Some(num_threads);
775 self
776 }
777 pub fn build(self) -> error::Result<DEConfig> {
783 if self.cfg.popsize < 4 {
784 return Err(DEError::PopulationTooSmall {
785 pop_size: self.cfg.popsize,
786 });
787 }
788 Ok(self.cfg)
789 }
790}
791
792#[derive(Clone)]
796pub struct DEReport {
797 pub x: Array1<f64>,
799 pub fun: f64,
801 pub success: bool,
803 pub message: String,
805 pub nit: usize,
807 pub nfev: usize,
809 pub population: Array2<f64>,
811 pub population_energies: Array1<f64>,
813}
814
815impl fmt::Debug for DEReport {
816 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
817 f.debug_struct("DEReport")
818 .field("x", &format!("len={}", self.x.len()))
819 .field("fun", &self.fun)
820 .field("success", &self.success)
821 .field("message", &self.message)
822 .field("nit", &self.nit)
823 .field("nfev", &self.nfev)
824 .field(
825 "population",
826 &format!("{}x{}", self.population.nrows(), self.population.ncols()),
827 )
828 .field(
829 "population_energies",
830 &format!("len={}", self.population_energies.len()),
831 )
832 .finish()
833 }
834}
835
836pub struct DEIntermediate {
838 pub x: Array1<f64>,
840 pub fun: f64,
842 pub convergence: f64,
844 pub iter: usize,
846}
847
848pub enum CallbackAction {
850 Continue,
852 Stop,
854}
855
856pub struct DifferentialEvolution<'a, F>
862where
863 F: Fn(&Array1<f64>) -> f64 + Sync,
864{
865 func: &'a F,
866 lower: Array1<f64>,
867 upper: Array1<f64>,
868 config: DEConfig,
869}
870
871impl<'a, F> DifferentialEvolution<'a, F>
872where
873 F: Fn(&Array1<f64>) -> f64 + Sync,
874{
875 pub fn new(func: &'a F, lower: Array1<f64>, upper: Array1<f64>) -> Result<Self> {
882 if lower.len() != upper.len() {
883 return Err(DEError::BoundsMismatch {
884 lower_len: lower.len(),
885 upper_len: upper.len(),
886 });
887 }
888
889 for i in 0..lower.len() {
891 if lower[i] > upper[i] {
892 return Err(DEError::InvalidBounds {
893 index: i,
894 lower: lower[i],
895 upper: upper[i],
896 });
897 }
898 }
899
900 Ok(Self {
901 func,
902 lower,
903 upper,
904 config: DEConfig::default(),
905 })
906 }
907
908 pub fn config_mut(&mut self) -> &mut DEConfig {
910 &mut self.config
911 }
912
913 pub fn solve(&mut self) -> DEReport {
915 use apply_integrality::apply_integrality;
916 use apply_wls::apply_wls;
917 use crossover_binomial::binomial_crossover;
918 use crossover_exponential::exponential_crossover;
919 use init_latin_hypercube::init_latin_hypercube;
920 use init_random::init_random;
921 use mutant_adaptive::mutant_adaptive;
922 use mutant_best1::mutant_best1;
923 use mutant_best2::mutant_best2;
924 use mutant_current_to_best1::mutant_current_to_best1;
925 use mutant_rand_to_best1::mutant_rand_to_best1;
926 use mutant_rand1::mutant_rand1;
927 use mutant_rand2::mutant_rand2;
928 use parallel_eval::evaluate_trials_parallel;
929 use std::sync::Arc;
930
931 let n = self.lower.len();
932
933 let mut is_free: Vec<bool> = Vec::with_capacity(n);
935 for i in 0..n {
936 is_free.push((self.upper[i] - self.lower[i]).abs() > 0.0);
937 }
938 let n_free = is_free.iter().filter(|&&b| b).count();
939 let _n_equal = n - n_free;
940 if n_free == 0 {
941 let x_fixed = self.lower.clone();
943 let mut x_eval = x_fixed.clone();
944 if let Some(mask) = &self.config.integrality {
945 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
946 }
947 let f = (self.func)(&x_eval);
948 return DEReport {
949 x: x_eval,
950 fun: f,
951 success: true,
952 message: "All variables fixed by bounds".into(),
953 nit: 0,
954 nfev: 1,
955 population: Array2::zeros((1, n)),
956 population_energies: Array1::from(vec![f]),
957 };
958 }
959
960 let npop = self.config.popsize * n_free;
961 let _bounds_span = &self.upper - &self.lower;
962
963 if self.config.disp {
964 eprintln!(
965 "DE Init: {} dimensions ({} free), population={}, maxiter={}",
966 n, n_free, npop, self.config.maxiter
967 );
968 eprintln!(
969 " Strategy: {:?}, Mutation: {:?}, Crossover: CR={:.3}",
970 self.config.strategy, self.config.mutation, self.config.recombination
971 );
972 eprintln!(
973 " Tolerances: tol={:.2e}, atol={:.2e}",
974 self.config.tol, self.config.atol
975 );
976 }
977
978 let timing_enabled = std::env::var("AUTOEQ_DE_TIMING")
980 .map(|v| v != "0")
981 .unwrap_or(false);
982
983 if let Some(n) = self.config.parallel.num_threads {
985 let _ = rayon::ThreadPoolBuilder::new()
987 .num_threads(n)
988 .build_global();
989 }
990
991 let mut rng: StdRng = match self.config.seed {
993 Some(s) => StdRng::seed_from_u64(s),
994 None => {
995 let mut thread_rng = rand::rng();
996 StdRng::from_rng(&mut thread_rng)
997 }
998 };
999
1000 let mut pop = match self.config.init {
1002 Init::LatinHypercube => {
1003 if self.config.disp {
1004 eprintln!(" Using Latin Hypercube initialization");
1005 }
1006 init_latin_hypercube(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1007 }
1008 Init::Random => {
1009 if self.config.disp {
1010 eprintln!(" Using Random initialization");
1011 }
1012 init_random(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1013 }
1014 };
1015
1016 let mut nfev: usize = 0;
1018 if self.config.disp {
1019 eprintln!(" Evaluating initial population of {} individuals...", npop);
1020 }
1021
1022 let mut eval_pop = pop.clone();
1024 let t_integrality0 = Instant::now();
1025 if let Some(mask) = &self.config.integrality {
1026 for i in 0..npop {
1027 let mut row = eval_pop.row_mut(i);
1028 let mut x_eval = row.to_owned();
1029 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1030 row.assign(&x_eval);
1031 }
1032 }
1033 let t_integrality = t_integrality0.elapsed();
1034
1035 let func_ref = self.func;
1037 let penalty_ineq_vec: Vec<PenaltyTuple> = self
1038 .config
1039 .penalty_ineq
1040 .iter()
1041 .map(|(f, w)| (f.clone(), *w))
1042 .collect();
1043 let penalty_eq_vec: Vec<PenaltyTuple> = self
1044 .config
1045 .penalty_eq
1046 .iter()
1047 .map(|(f, w)| (f.clone(), *w))
1048 .collect();
1049 let linear_penalty = self.config.linear_penalty.clone();
1050
1051 let energy_fn = Arc::new(move |x: &Array1<f64>| -> f64 {
1052 let base = (func_ref)(x);
1053 let mut p = 0.0;
1054 for (f, w) in &penalty_ineq_vec {
1055 let v = f(x);
1056 let viol = v.max(0.0);
1057 p += w * viol * viol;
1058 }
1059 for (h, w) in &penalty_eq_vec {
1060 let v = h(x);
1061 p += w * v * v;
1062 }
1063 if let Some(ref lp) = linear_penalty {
1064 let ax = lp.a.dot(&x.view());
1065 Zip::from(&ax)
1066 .and(&lp.lb)
1067 .and(&lp.ub)
1068 .for_each(|&v, &lo, &hi| {
1069 if v < lo {
1070 let d = lo - v;
1071 p += lp.weight * d * d;
1072 } else if v > hi {
1073 let d = v - hi;
1074 p += lp.weight * d * d;
1075 }
1076 });
1077 }
1078 base + p
1079 });
1080
1081 let t_eval0 = Instant::now();
1082 let mut energies = parallel_eval::evaluate_population_parallel(
1083 &eval_pop,
1084 energy_fn.clone(),
1085 &self.config.parallel,
1086 );
1087 let t_eval_init = t_eval0.elapsed();
1088 nfev += npop;
1089 if timing_enabled {
1090 eprintln!(
1091 "TIMING init: integrality={:.3} ms, eval={:.3} ms",
1092 t_integrality.as_secs_f64() * 1e3,
1093 t_eval_init.as_secs_f64() * 1e3
1094 );
1095 }
1096
1097 let pop_mean = energies.mean().unwrap_or(0.0);
1099 let pop_std = energies.std(0.0);
1100 if self.config.disp {
1101 eprintln!(
1102 " Initial population: mean={:.6e}, std={:.6e}",
1103 pop_mean, pop_std
1104 );
1105 }
1106
1107 if let Some(x0) = &self.config.x0 {
1109 let mut x0c = x0.clone();
1110 for i in 0..x0c.len() {
1112 x0c[i] = x0c[i].clamp(self.lower[i], self.upper[i]);
1113 }
1114 if let Some(mask) = &self.config.integrality {
1115 apply_integrality(&mut x0c, mask, &self.lower, &self.upper);
1116 }
1117 let f0 = self.energy(&x0c);
1118 nfev += 1;
1119 let (best_idx, _best_f) = argmin(&energies);
1121 pop.row_mut(best_idx).assign(&x0c.view());
1122 energies[best_idx] = f0;
1123 }
1124
1125 let (mut best_idx, mut best_f) = argmin(&energies);
1126 let mut best_x = pop.row(best_idx).to_owned();
1127
1128 if self.config.disp {
1129 eprintln!(
1130 " Initial best: fitness={:.6e} at index {}",
1131 best_f, best_idx
1132 );
1133 let param_summary: Vec<String> = (0..best_x.len() / 3)
1134 .map(|i| {
1135 let freq = 10f64.powf(best_x[i * 3]);
1136 let q = best_x[i * 3 + 1];
1137 let gain = best_x[i * 3 + 2];
1138 format!("f{:.0}Hz/Q{:.2}/G{:.2}dB", freq, q, gain)
1139 })
1140 .collect();
1141 eprintln!(" Initial best params: [{}]", param_summary.join(", "));
1142 }
1143
1144 if self.config.disp {
1145 eprintln!("DE iter {:4} best_f={:.6e}", 0, best_f);
1146 }
1147
1148 let mut adaptive_state = if matches!(
1150 self.config.strategy,
1151 Strategy::AdaptiveBin | Strategy::AdaptiveExp
1152 ) || self.config.adaptive.adaptive_mutation
1153 {
1154 Some(AdaptiveState::new(&self.config.adaptive))
1155 } else {
1156 None
1157 };
1158
1159 let mut success = false;
1161 let mut message = String::new();
1162 let mut nit = 0;
1163 let mut accepted_trials;
1164 let mut improvement_count;
1165
1166 let mut t_build_tot = std::time::Duration::ZERO;
1167 let mut t_eval_tot = std::time::Duration::ZERO;
1168 let mut t_select_tot = std::time::Duration::ZERO;
1169 let mut t_iter_tot = std::time::Duration::ZERO;
1170
1171 for iter in 1..=self.config.maxiter {
1172 nit = iter;
1173 accepted_trials = 0;
1174 improvement_count = 0;
1175
1176 let iter_start = Instant::now();
1177
1178 let sorted_indices = if matches!(
1180 self.config.strategy,
1181 Strategy::AdaptiveBin | Strategy::AdaptiveExp
1182 ) {
1183 let mut indices: Vec<usize> = (0..npop).collect();
1184 indices.sort_by(|&a, &b| {
1185 energies[a]
1186 .partial_cmp(&energies[b])
1187 .unwrap_or(std::cmp::Ordering::Equal)
1188 });
1189 indices
1190 } else {
1191 Vec::new() };
1193
1194 let t_build0 = Instant::now();
1196
1197 use rayon::prelude::*;
1199 let (trials, trial_params): (Vec<_>, Vec<_>) = (0..npop)
1200 .into_par_iter()
1201 .map(|i| {
1202 let mut local_rng: StdRng = if let Some(base_seed) = self.config.seed {
1204 StdRng::seed_from_u64(
1205 base_seed
1206 .wrapping_add((iter as u64) << 32)
1207 .wrapping_add(i as u64),
1208 )
1209 } else {
1210 let mut thread_rng = rand::rng();
1212 StdRng::from_rng(&mut thread_rng)
1213 };
1214
1215 let (f, cr) = if let Some(ref adaptive) = adaptive_state {
1217 let adaptive_f = adaptive.sample_f(&mut local_rng);
1219 let adaptive_cr = adaptive.sample_cr(&mut local_rng);
1220 (adaptive_f, adaptive_cr)
1221 } else {
1222 (
1224 self.config.mutation.sample(&mut local_rng),
1225 self.config.recombination,
1226 )
1227 };
1228
1229 let (mutant, cross) = match self.config.strategy {
1231 Strategy::Best1Bin => (
1232 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1233 Crossover::Binomial,
1234 ),
1235 Strategy::Best1Exp => (
1236 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1237 Crossover::Exponential,
1238 ),
1239 Strategy::Rand1Bin => (
1240 mutant_rand1(i, &pop, f, &mut local_rng),
1241 Crossover::Binomial,
1242 ),
1243 Strategy::Rand1Exp => (
1244 mutant_rand1(i, &pop, f, &mut local_rng),
1245 Crossover::Exponential,
1246 ),
1247 Strategy::Rand2Bin => (
1248 mutant_rand2(i, &pop, f, &mut local_rng),
1249 Crossover::Binomial,
1250 ),
1251 Strategy::Rand2Exp => (
1252 mutant_rand2(i, &pop, f, &mut local_rng),
1253 Crossover::Exponential,
1254 ),
1255 Strategy::CurrentToBest1Bin => (
1256 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1257 Crossover::Binomial,
1258 ),
1259 Strategy::CurrentToBest1Exp => (
1260 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1261 Crossover::Exponential,
1262 ),
1263 Strategy::Best2Bin => (
1264 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1265 Crossover::Binomial,
1266 ),
1267 Strategy::Best2Exp => (
1268 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1269 Crossover::Exponential,
1270 ),
1271 Strategy::RandToBest1Bin => (
1272 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1273 Crossover::Binomial,
1274 ),
1275 Strategy::RandToBest1Exp => (
1276 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1277 Crossover::Exponential,
1278 ),
1279 Strategy::AdaptiveBin => {
1280 if let Some(ref adaptive) = adaptive_state {
1281 (
1282 mutant_adaptive(
1283 i,
1284 &pop,
1285 &sorted_indices,
1286 adaptive.current_w,
1287 f,
1288 &mut local_rng,
1289 ),
1290 Crossover::Binomial,
1291 )
1292 } else {
1293 (
1295 mutant_rand1(i, &pop, f, &mut local_rng),
1296 Crossover::Binomial,
1297 )
1298 }
1299 }
1300 Strategy::AdaptiveExp => {
1301 if let Some(ref adaptive) = adaptive_state {
1302 (
1303 mutant_adaptive(
1304 i,
1305 &pop,
1306 &sorted_indices,
1307 adaptive.current_w,
1308 f,
1309 &mut local_rng,
1310 ),
1311 Crossover::Exponential,
1312 )
1313 } else {
1314 (
1316 mutant_rand1(i, &pop, f, &mut local_rng),
1317 Crossover::Exponential,
1318 )
1319 }
1320 }
1321 };
1322
1323 let crossover = cross;
1325 let trial = match crossover {
1326 Crossover::Binomial => {
1327 binomial_crossover(&pop.row(i).to_owned(), &mutant, cr, &mut local_rng)
1328 }
1329 Crossover::Exponential => exponential_crossover(
1330 &pop.row(i).to_owned(),
1331 &mutant,
1332 cr,
1333 &mut local_rng,
1334 ),
1335 };
1336
1337 let wls_trial = if self.config.adaptive.wls_enabled
1339 && local_rng.random::<f64>() < self.config.adaptive.wls_prob
1340 {
1341 apply_wls(
1342 &trial,
1343 &self.lower,
1344 &self.upper,
1345 self.config.adaptive.wls_scale,
1346 &mut local_rng,
1347 )
1348 } else {
1349 trial.clone()
1350 };
1351
1352 let mut trial_clipped = wls_trial;
1354 Zip::from(&mut trial_clipped)
1355 .and(&self.lower)
1356 .and(&self.upper)
1357 .for_each(|x, lo, hi| *x = x.clamp(*lo, *hi));
1358
1359 if let Some(mask) = &self.config.integrality {
1361 apply_integrality(&mut trial_clipped, mask, &self.lower, &self.upper);
1362 }
1363
1364 (trial_clipped, (f, cr))
1366 })
1367 .unzip();
1368
1369 let t_build = t_build0.elapsed();
1370 let t_eval0 = Instant::now();
1371 let trial_energies =
1372 evaluate_trials_parallel(&trials, energy_fn.clone(), &self.config.parallel);
1373 let t_eval = t_eval0.elapsed();
1374 nfev += npop;
1375
1376 let t_select0 = Instant::now();
1377 for (i, (trial, trial_energy)) in
1379 trials.into_iter().zip(trial_energies.iter()).enumerate()
1380 {
1381 let (f, cr) = trial_params[i];
1382
1383 if *trial_energy <= energies[i] {
1385 pop.row_mut(i).assign(&trial.view());
1386 energies[i] = *trial_energy;
1387 accepted_trials += 1;
1388
1389 if let Some(ref mut adaptive) = adaptive_state {
1391 adaptive.record_success(f, cr);
1392 }
1393
1394 if *trial_energy < best_f {
1396 improvement_count += 1;
1397 }
1398 }
1399 }
1400 let t_select = t_select0.elapsed();
1401
1402 t_build_tot += t_build;
1403 t_eval_tot += t_eval;
1404 t_select_tot += t_select;
1405 let iter_dur = iter_start.elapsed();
1406 t_iter_tot += iter_dur;
1407
1408 if timing_enabled && (iter <= 5 || iter % 10 == 0) {
1409 eprintln!(
1410 "TIMING iter {:4}: build={:.3} ms, eval={:.3} ms, select={:.3} ms, total={:.3} ms",
1411 iter,
1412 t_build.as_secs_f64() * 1e3,
1413 t_eval.as_secs_f64() * 1e3,
1414 t_select.as_secs_f64() * 1e3,
1415 iter_dur.as_secs_f64() * 1e3,
1416 );
1417 }
1418
1419 if let Some(ref mut adaptive) = adaptive_state {
1421 adaptive.update(&self.config.adaptive, iter, self.config.maxiter);
1422 }
1423
1424 let (new_best_idx, new_best_f) = argmin(&energies);
1426 if new_best_f < best_f {
1427 best_idx = new_best_idx;
1428 best_f = new_best_f;
1429 best_x = pop.row(best_idx).to_owned();
1430 }
1431
1432 let pop_mean = energies.mean().unwrap_or(0.0);
1434 let pop_std = energies.std(0.0);
1435 let convergence_threshold = self.config.atol + self.config.tol * pop_mean.abs();
1436
1437 if self.config.disp {
1438 eprintln!(
1439 "DE iter {:4} best_f={:.6e} std={:.3e} accepted={}/{}, improved={}",
1440 iter, best_f, pop_std, accepted_trials, npop, improvement_count
1441 );
1442 }
1443
1444 if let Some(ref mut cb) = self.config.callback {
1446 let intermediate = DEIntermediate {
1447 x: best_x.clone(),
1448 fun: best_f,
1449 convergence: pop_std,
1450 iter,
1451 };
1452 match cb(&intermediate) {
1453 CallbackAction::Stop => {
1454 success = true;
1455 message = "Optimization stopped by callback".to_string();
1456 break;
1457 }
1458 CallbackAction::Continue => {}
1459 }
1460 }
1461
1462 if pop_std <= convergence_threshold {
1463 success = true;
1464 message = format!(
1465 "Converged: std(pop_f)={:.3e} <= threshold={:.3e}",
1466 pop_std, convergence_threshold
1467 );
1468 break;
1469 }
1470 }
1471
1472 if !success {
1473 message = format!("Maximum iterations reached: {}", self.config.maxiter);
1474 }
1475
1476 if self.config.disp {
1477 eprintln!("DE finished: {}", message);
1478 }
1479
1480 let (final_x, final_f, polish_nfev) = if let Some(ref polish_cfg) = self.config.polish {
1482 if polish_cfg.enabled {
1483 self.polish(&best_x)
1484 } else {
1485 (best_x.clone(), best_f, 0)
1486 }
1487 } else {
1488 (best_x.clone(), best_f, 0)
1489 };
1490
1491 if timing_enabled {
1492 eprintln!(
1493 "TIMING total: build={:.3} s, eval={:.3} s, select={:.3} s, iter_total={:.3} s",
1494 t_build_tot.as_secs_f64(),
1495 t_eval_tot.as_secs_f64(),
1496 t_select_tot.as_secs_f64(),
1497 t_iter_tot.as_secs_f64()
1498 );
1499 }
1500
1501 self.finish_report(
1502 pop,
1503 energies,
1504 final_x,
1505 final_f,
1506 success,
1507 message,
1508 nit,
1509 nfev + polish_nfev,
1510 )
1511 }
1512}
1513
1514#[cfg(test)]
1515mod strategy_tests {
1516 use super::*;
1517
1518 #[test]
1519 fn test_parse_strategy_variants() {
1520 assert!(matches!(
1521 "best1exp".parse::<Strategy>().unwrap(),
1522 Strategy::Best1Exp
1523 ));
1524 assert!(matches!(
1525 "rand1bin".parse::<Strategy>().unwrap(),
1526 Strategy::Rand1Bin
1527 ));
1528 assert!(matches!(
1529 "randtobest1exp".parse::<Strategy>().unwrap(),
1530 Strategy::RandToBest1Exp
1531 ));
1532 }
1533}