1#![doc = include_str!("../README.md")]
72#![doc = include_str!("../REFERENCES.md")]
73#![warn(missing_docs)]
74
75pub mod error;
76pub mod levenberg_marquardt;
77pub use error::{DEError, Result};
78pub use levenberg_marquardt::{
79 LMCallbackAction, LMConfig, LMConfigBuilder, LMError, LMIntermediate, LMReport, LMResult,
80 levenberg_marquardt,
81};
82
83use std::fmt;
84use std::str::FromStr;
85use std::sync::Arc;
86use std::sync::RwLock;
87
88use ndarray::{Array1, Array2, Zip};
89use oxiblas_ndarray::blas::matvec;
90use rand::rngs::StdRng;
91use rand::{Rng, SeedableRng};
92use std::time::Instant;
93
94pub mod stack_linear_penalty;
96
97pub mod apply_integrality;
99pub mod apply_wls;
101
102pub mod distinct_indices;
104pub mod external_archive;
106pub mod init_latin_hypercube;
108pub mod init_random;
110pub mod init_sobol;
112pub mod lshade;
114
115pub mod mutant_adaptive;
117pub mod mutant_best1;
119pub mod mutant_best2;
121pub mod mutant_current_to_best1;
123pub mod mutant_current_to_pbest1;
125pub mod mutant_rand1;
127pub mod mutant_rand2;
129pub mod mutant_rand_to_best1;
131
132pub mod crossover_binomial;
134pub mod crossover_exponential;
136
137#[cfg(test)]
139mod de_tests;
140pub mod differential_evolution;
142pub mod function_registry;
144pub mod impl_helpers;
146pub mod metadata;
148pub mod parallel_eval;
150pub mod recorder;
152pub mod run_recorded;
154pub use differential_evolution::differential_evolution;
155pub use external_archive::ExternalArchive;
156pub use lshade::LShadeConfig;
157pub use parallel_eval::ParallelConfig;
158pub use recorder::{OptimizationRecord, OptimizationRecorder};
159pub use run_recorded::run_recorded_differential_evolution;
160
161pub type ScalarConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
164pub type VectorConstraintFn = Arc<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
166pub type PenaltyTuple = (ScalarConstraintFn, f64);
168pub type CallbackFn = Box<dyn FnMut(&DEIntermediate) -> CallbackAction>;
170
171pub(crate) fn argmin(v: &Array1<f64>) -> (usize, f64) {
172 let mut best_i = 0usize;
173 let mut best_v = v[0];
174 for (i, &val) in v.iter().enumerate() {
175 if val < best_v {
176 best_v = val;
177 best_i = i;
178 }
179 }
180 (best_i, best_v)
181}
182
183#[derive(Debug, Clone, Copy)]
190pub enum Strategy {
191 Best1Bin,
193 Best1Exp,
195 Rand1Bin,
197 Rand1Exp,
199 Rand2Bin,
201 Rand2Exp,
203 CurrentToBest1Bin,
205 CurrentToBest1Exp,
207 Best2Bin,
209 Best2Exp,
211 RandToBest1Bin,
213 RandToBest1Exp,
215 AdaptiveBin,
217 AdaptiveExp,
219 LShadeBin,
221 LShadeExp,
223}
224
225impl FromStr for Strategy {
226 type Err = String;
227 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
228 let t = s.to_lowercase();
229 match t.as_str() {
230 "best1bin" | "best1" => Ok(Strategy::Best1Bin),
231 "best1exp" => Ok(Strategy::Best1Exp),
232 "rand1bin" | "rand1" => Ok(Strategy::Rand1Bin),
233 "rand1exp" => Ok(Strategy::Rand1Exp),
234 "rand2bin" | "rand2" => Ok(Strategy::Rand2Bin),
235 "rand2exp" => Ok(Strategy::Rand2Exp),
236 "currenttobest1bin" | "current-to-best1bin" | "current_to_best1bin" => {
237 Ok(Strategy::CurrentToBest1Bin)
238 }
239 "currenttobest1exp" | "current-to-best1exp" | "current_to_best1exp" => {
240 Ok(Strategy::CurrentToBest1Exp)
241 }
242 "best2bin" | "best2" => Ok(Strategy::Best2Bin),
243 "best2exp" => Ok(Strategy::Best2Exp),
244 "randtobest1bin" | "rand-to-best1bin" | "rand_to_best1bin" => {
245 Ok(Strategy::RandToBest1Bin)
246 }
247 "randtobest1exp" | "rand-to-best1exp" | "rand_to_best1exp" => {
248 Ok(Strategy::RandToBest1Exp)
249 }
250 "adaptivebin" | "adaptive-bin" | "adaptive_bin" | "adaptive" => {
251 Ok(Strategy::AdaptiveBin)
252 }
253 "adaptiveexp" | "adaptive-exp" | "adaptive_exp" => Ok(Strategy::AdaptiveExp),
254 "lshadebin" | "lshade-bin" | "lshade_bin" | "lshade" => Ok(Strategy::LShadeBin),
255 "lshadeexp" | "lshade-exp" | "lshade_exp" => Ok(Strategy::LShadeExp),
256 _ => Err(format!("unknown strategy: {}", s)),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy, Default)]
263pub enum Crossover {
264 #[default]
266 Binomial,
267 Exponential,
269}
270
271#[derive(Debug, Clone, Copy)]
273pub enum Mutation {
274 Factor(f64),
276 Range {
278 min: f64,
280 max: f64,
282 },
283 Adaptive {
285 initial_f: f64,
287 },
288}
289
290impl Default for Mutation {
291 fn default() -> Self {
292 let _ = Mutation::Factor(0.8);
293 Mutation::Range { min: 0.0, max: 2.0 }
294 }
295}
296
297impl Mutation {
298 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
299 match *self {
300 Mutation::Factor(f) => f,
301 Mutation::Range { min, max } => rng.random_range(min..max),
302 Mutation::Adaptive { initial_f } => initial_f, }
304 }
305
306 #[allow(dead_code)]
308 fn sample_cauchy<R: Rng + ?Sized>(&self, f_m: f64, _scale: f64, rng: &mut R) -> f64 {
309 let perturbation = (rng.random::<f64>() - 0.5) * 0.2; (f_m + perturbation).clamp(0.0, 2.0) }
313}
314
315#[derive(Debug, Clone, Copy, Default)]
317pub enum Init {
318 #[default]
320 LatinHypercube,
321 Random,
323}
324
325#[derive(Debug, Clone, Copy, Default)]
327pub enum Updating {
328 #[default]
330 Deferred,
331}
332
333#[derive(Debug, Clone)]
335pub struct LinearPenalty {
336 pub a: Array2<f64>,
338 pub lb: Array1<f64>,
340 pub ub: Array1<f64>,
342 pub weight: f64,
344}
345
346#[derive(Debug, Clone)]
348pub struct LinearConstraintHelper {
349 pub a: Array2<f64>,
351 pub lb: Array1<f64>,
353 pub ub: Array1<f64>,
355}
356
357impl LinearConstraintHelper {
358 pub fn apply_to(&self, cfg: &mut DEConfig, weight: f64) {
360 use stack_linear_penalty::stack_linear_penalty;
361
362 let new_lp = LinearPenalty {
363 a: self.a.clone(),
364 lb: self.lb.clone(),
365 ub: self.ub.clone(),
366 weight,
367 };
368 match &mut cfg.linear_penalty {
369 Some(existing) => stack_linear_penalty(existing, &new_lp),
370 None => cfg.linear_penalty = Some(new_lp),
371 }
372 }
373}
374
375#[derive(Clone)]
377pub struct NonlinearConstraintHelper {
378 pub fun: VectorConstraintFn,
380 pub lb: Array1<f64>,
382 pub ub: Array1<f64>,
384}
385
386impl NonlinearConstraintHelper {
387 pub fn apply_to(&self, cfg: &mut DEConfig, weight_ineq: f64, weight_eq: f64) {
391 let f = self.fun.clone();
392 let lb = self.lb.clone();
393 let ub = self.ub.clone();
394 let m = lb.len().min(ub.len());
395 for i in 0..m {
396 let l = lb[i];
397 let u = ub[i];
398 let tol = 1e-12 * (l.abs() + u.abs()).max(1.0);
399 if (u - l).abs() <= tol {
400 let fi = f.clone();
401 cfg.penalty_eq.push((
402 Arc::new(move |x: &Array1<f64>| {
403 let y = (fi)(x);
404 y[i] - l
405 }),
406 weight_eq,
407 ));
408 } else {
409 let fi_u = f.clone();
410 cfg.penalty_ineq.push((
411 Arc::new(move |x: &Array1<f64>| {
412 let y = (fi_u)(x);
413 y[i] - u
414 }),
415 weight_ineq,
416 ));
417 let fi_l = f.clone();
418 cfg.penalty_ineq.push((
419 Arc::new(move |x: &Array1<f64>| {
420 let y = (fi_l)(x);
421 l - y[i]
422 }),
423 weight_ineq,
424 ));
425 }
426 }
427 }
428}
429
430#[derive(Debug, Clone)]
432struct AdaptiveState {
433 f_m: f64,
435 cr_m: f64,
437 successful_f: Vec<f64>,
439 successful_cr: Vec<f64>,
441 current_w: f64,
443}
444
445impl AdaptiveState {
446 fn new(config: &AdaptiveConfig) -> Self {
447 Self {
448 f_m: config.f_m,
449 cr_m: config.cr_m,
450 successful_f: Vec::new(),
451 successful_cr: Vec::new(),
452 current_w: config.w_max, }
454 }
455
456 fn update(&mut self, config: &AdaptiveConfig, iter: usize, max_iter: usize) {
458 let iter_ratio = iter as f64 / max_iter as f64;
460 self.current_w = config.w_max - (config.w_max - config.w_min) * iter_ratio;
461
462 if !self.successful_f.is_empty() {
464 let mean_f = self.compute_lehmer_mean(&self.successful_f);
465 self.f_m = (1.0 - config.w_f) * self.f_m + config.w_f * mean_f;
466 self.f_m = self.f_m.clamp(0.2, 1.2);
467 }
468
469 if !self.successful_cr.is_empty() {
471 let mean_cr = self.compute_arithmetic_mean(&self.successful_cr);
472 self.cr_m = (1.0 - config.w_cr) * self.cr_m + config.w_cr * mean_cr;
473 self.cr_m = self.cr_m.clamp(0.1, 0.9);
474 }
475
476 self.successful_f.clear();
478 self.successful_cr.clear();
479 }
480
481 fn compute_lehmer_mean(&self, values: &[f64]) -> f64 {
483 if values.is_empty() {
484 return 0.5; }
486
487 let sum_sq: f64 = values.iter().map(|&x| x * x).sum();
488 let sum: f64 = values.iter().sum();
489
490 if sum > 0.0 {
491 sum_sq / sum
492 } else {
493 0.5 }
495 }
496
497 fn compute_arithmetic_mean(&self, values: &[f64]) -> f64 {
499 if values.is_empty() {
500 return 0.5; }
502 values.iter().sum::<f64>() / values.len() as f64
503 }
504
505 fn record_success(&mut self, f_val: f64, cr_val: f64) {
507 self.successful_f.push(f_val);
508 self.successful_cr.push(cr_val);
509 }
510
511 fn sample_f<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
513 let u1: f64 = rng.random::<f64>().max(1e-15);
514 let u2: f64 = rng.random::<f64>();
515
516 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
517 let sample = self.f_m + 0.05 * normal;
518
519 sample.clamp(0.3, 1.0)
520 }
521
522 fn sample_cr<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
524 let u1: f64 = rng.random::<f64>().max(1e-15);
525 let u2: f64 = rng.random::<f64>();
526
527 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
528 let sample = self.cr_m + 0.05 * normal;
529
530 sample.clamp(0.1, 0.9)
531 }
532}
533
534#[derive(Debug, Clone)]
536pub struct AdaptiveConfig {
537 pub adaptive_mutation: bool,
539 pub wls_enabled: bool,
541 pub w_max: f64,
543 pub w_min: f64,
545 pub w_f: f64,
547 pub w_cr: f64,
549 pub f_m: f64,
551 pub cr_m: f64,
553 pub wls_prob: f64,
555 pub wls_scale: f64,
557}
558
559impl Default for AdaptiveConfig {
560 fn default() -> Self {
561 Self {
562 adaptive_mutation: false,
563 wls_enabled: false,
564 w_max: 0.9,
565 w_min: 0.1,
566 w_f: 0.9,
567 w_cr: 0.9,
568 f_m: 0.5,
569 cr_m: 0.6,
570 wls_prob: 0.1,
571 wls_scale: 0.1,
572 }
573 }
574}
575
576#[derive(Debug, Clone)]
578pub struct PolishConfig {
579 pub enabled: bool,
581 pub algo: String,
583 pub maxeval: usize,
585}
586
587pub struct DEConfig {
593 pub maxiter: usize,
595 pub popsize: usize,
597 pub tol: f64,
599 pub atol: f64,
601 pub mutation: Mutation,
603 pub recombination: f64,
605 pub strategy: Strategy,
607 pub crossover: Crossover,
609 pub init: Init,
611 pub updating: Updating,
613 pub seed: Option<u64>,
615 pub integrality: Option<Vec<bool>>,
617 pub x0: Option<Array1<f64>>,
619 pub disp: bool,
621 pub callback: Option<CallbackFn>,
623 pub penalty_ineq: Vec<PenaltyTuple>,
625 pub penalty_eq: Vec<PenaltyTuple>,
627 pub linear_penalty: Option<LinearPenalty>,
629 pub polish: Option<PolishConfig>,
631 pub adaptive: AdaptiveConfig,
633 pub parallel: parallel_eval::ParallelConfig,
635 pub lshade: lshade::LShadeConfig,
637}
638
639impl Default for DEConfig {
640 fn default() -> Self {
641 Self {
642 maxiter: 1000,
643 popsize: 15,
644 tol: 1e-2,
645 atol: 0.0,
646 mutation: Mutation::default(),
647 recombination: 0.7,
648 strategy: Strategy::Best1Bin,
649 crossover: Crossover::default(),
650 init: Init::default(),
651 updating: Updating::default(),
652 seed: None,
653 integrality: None,
654 x0: None,
655 disp: false,
656 callback: None,
657 penalty_ineq: Vec::new(),
658 penalty_eq: Vec::new(),
659 linear_penalty: None,
660 polish: None,
661 adaptive: AdaptiveConfig::default(),
662 parallel: parallel_eval::ParallelConfig::default(),
663 lshade: lshade::LShadeConfig::default(),
664 }
665 }
666}
667
668pub struct DEConfigBuilder {
685 cfg: DEConfig,
686}
687impl Default for DEConfigBuilder {
688 fn default() -> Self {
689 Self::new()
690 }
691}
692
693impl DEConfigBuilder {
694 pub fn new() -> Self {
696 Self {
697 cfg: DEConfig::default(),
698 }
699 }
700 pub fn maxiter(mut self, v: usize) -> Self {
702 self.cfg.maxiter = v;
703 self
704 }
705 pub fn popsize(mut self, v: usize) -> Self {
712 self.cfg.popsize = v;
713 self
714 }
715 pub fn tol(mut self, v: f64) -> Self {
717 self.cfg.tol = v;
718 self
719 }
720 pub fn atol(mut self, v: f64) -> Self {
722 self.cfg.atol = v;
723 self
724 }
725 pub fn mutation(mut self, v: Mutation) -> Self {
727 self.cfg.mutation = v;
728 self
729 }
730 pub fn recombination(mut self, v: f64) -> Self {
732 self.cfg.recombination = v;
733 self
734 }
735 pub fn strategy(mut self, v: Strategy) -> Self {
737 self.cfg.strategy = v;
738 self
739 }
740 pub fn crossover(mut self, v: Crossover) -> Self {
742 self.cfg.crossover = v;
743 self
744 }
745 pub fn init(mut self, v: Init) -> Self {
747 self.cfg.init = v;
748 self
749 }
750 pub fn seed(mut self, v: u64) -> Self {
752 self.cfg.seed = Some(v);
753 self
754 }
755 pub fn integrality(mut self, v: Vec<bool>) -> Self {
757 self.cfg.integrality = Some(v);
758 self
759 }
760 pub fn x0(mut self, v: Array1<f64>) -> Self {
762 self.cfg.x0 = Some(v);
763 self
764 }
765 pub fn disp(mut self, v: bool) -> Self {
767 self.cfg.disp = v;
768 self
769 }
770 pub fn callback(mut self, cb: Box<dyn FnMut(&DEIntermediate) -> CallbackAction>) -> Self {
772 self.cfg.callback = Some(cb);
773 self
774 }
775 pub fn add_penalty_ineq<FN>(mut self, f: FN, w: f64) -> Self
777 where
778 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
779 {
780 self.cfg.penalty_ineq.push((Arc::new(f), w));
781 self
782 }
783 pub fn add_penalty_eq<FN>(mut self, f: FN, w: f64) -> Self
785 where
786 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
787 {
788 self.cfg.penalty_eq.push((Arc::new(f), w));
789 self
790 }
791 pub fn linear_penalty(mut self, lp: LinearPenalty) -> Self {
793 self.cfg.linear_penalty = Some(lp);
794 self
795 }
796 pub fn polish(mut self, pol: PolishConfig) -> Self {
798 self.cfg.polish = Some(pol);
799 self
800 }
801 pub fn adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
803 self.cfg.adaptive = adaptive;
804 self
805 }
806 pub fn enable_adaptive_mutation(mut self, enable: bool) -> Self {
808 self.cfg.adaptive.adaptive_mutation = enable;
809 self
810 }
811 pub fn enable_wls(mut self, enable: bool) -> Self {
813 self.cfg.adaptive.wls_enabled = enable;
814 self
815 }
816 pub fn adaptive_weights(mut self, w_max: f64, w_min: f64) -> Self {
818 self.cfg.adaptive.w_max = w_max;
819 self.cfg.adaptive.w_min = w_min;
820 self
821 }
822 pub fn parallel(mut self, parallel: parallel_eval::ParallelConfig) -> Self {
824 self.cfg.parallel = parallel;
825 self
826 }
827 pub fn enable_parallel(mut self, enable: bool) -> Self {
829 self.cfg.parallel.enabled = enable;
830 self
831 }
832 pub fn parallel_threads(mut self, num_threads: usize) -> Self {
834 self.cfg.parallel.num_threads = Some(num_threads);
835 self
836 }
837 pub fn lshade(mut self, lshade: lshade::LShadeConfig) -> Self {
839 self.cfg.lshade = lshade;
840 self
841 }
842 pub fn build(self) -> error::Result<DEConfig> {
848 if self.cfg.popsize < 4 {
849 return Err(DEError::PopulationTooSmall {
850 pop_size: self.cfg.popsize,
851 });
852 }
853 Ok(self.cfg)
854 }
855}
856
857#[derive(Clone)]
861pub struct DEReport {
862 pub x: Array1<f64>,
864 pub fun: f64,
866 pub success: bool,
868 pub message: String,
870 pub nit: usize,
872 pub nfev: usize,
874 pub population: Array2<f64>,
876 pub population_energies: Array1<f64>,
878}
879
880impl fmt::Debug for DEReport {
881 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
882 f.debug_struct("DEReport")
883 .field("x", &format!("len={}", self.x.len()))
884 .field("fun", &self.fun)
885 .field("success", &self.success)
886 .field("message", &self.message)
887 .field("nit", &self.nit)
888 .field("nfev", &self.nfev)
889 .field(
890 "population",
891 &format!("{}x{}", self.population.nrows(), self.population.ncols()),
892 )
893 .field(
894 "population_energies",
895 &format!("len={}", self.population_energies.len()),
896 )
897 .finish()
898 }
899}
900
901pub struct DEIntermediate {
903 pub x: Array1<f64>,
905 pub fun: f64,
907 pub convergence: f64,
909 pub iter: usize,
911}
912
913pub enum CallbackAction {
915 Continue,
917 Stop,
919}
920
921pub struct DifferentialEvolution<'a, F>
927where
928 F: Fn(&Array1<f64>) -> f64 + Sync,
929{
930 func: &'a F,
931 lower: Array1<f64>,
932 upper: Array1<f64>,
933 config: DEConfig,
934}
935
936impl<'a, F> DifferentialEvolution<'a, F>
937where
938 F: Fn(&Array1<f64>) -> f64 + Sync,
939{
940 pub fn new(func: &'a F, lower: Array1<f64>, upper: Array1<f64>) -> Result<Self> {
947 if lower.len() != upper.len() {
948 return Err(DEError::BoundsMismatch {
949 lower_len: lower.len(),
950 upper_len: upper.len(),
951 });
952 }
953
954 for i in 0..lower.len() {
956 if lower[i] > upper[i] {
957 return Err(DEError::InvalidBounds {
958 index: i,
959 lower: lower[i],
960 upper: upper[i],
961 });
962 }
963 }
964
965 Ok(Self {
966 func,
967 lower,
968 upper,
969 config: DEConfig::default(),
970 })
971 }
972
973 pub fn config_mut(&mut self) -> &mut DEConfig {
975 &mut self.config
976 }
977
978 pub fn solve(&mut self) -> DEReport {
980 use apply_integrality::apply_integrality;
981 use apply_wls::apply_wls;
982 use crossover_binomial::binomial_crossover;
983 use crossover_exponential::exponential_crossover;
984 use init_latin_hypercube::init_latin_hypercube;
985 use init_random::init_random;
986 use mutant_adaptive::mutant_adaptive;
987 use mutant_best1::mutant_best1;
988 use mutant_best2::mutant_best2;
989 use mutant_current_to_best1::mutant_current_to_best1;
990 use mutant_current_to_pbest1;
991 use mutant_rand_to_best1::mutant_rand_to_best1;
992 use mutant_rand1::mutant_rand1;
993 use mutant_rand2::mutant_rand2;
994 use parallel_eval::evaluate_trials_parallel;
995 use std::sync::Arc;
996
997 let n = self.lower.len();
998
999 let mut is_free: Vec<bool> = Vec::with_capacity(n);
1001 for i in 0..n {
1002 is_free.push((self.upper[i] - self.lower[i]).abs() > 0.0);
1003 }
1004 let n_free = is_free.iter().filter(|&&b| b).count();
1005 let _n_equal = n - n_free;
1006 if n_free == 0 {
1007 let x_fixed = self.lower.clone();
1009 let mut x_eval = x_fixed.clone();
1010 if let Some(mask) = &self.config.integrality {
1011 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1012 }
1013 let f = (self.func)(&x_eval);
1014 return DEReport {
1015 x: x_eval,
1016 fun: f,
1017 success: true,
1018 message: "All variables fixed by bounds".into(),
1019 nit: 0,
1020 nfev: 1,
1021 population: Array2::zeros((1, n)),
1022 population_energies: Array1::from(vec![f]),
1023 };
1024 }
1025
1026 let npop = self.config.popsize * n_free;
1027 let _bounds_span = &self.upper - &self.lower;
1028
1029 if self.config.disp {
1030 eprintln!(
1031 "DE Init: {} dimensions ({} free), population={}, maxiter={}",
1032 n, n_free, npop, self.config.maxiter
1033 );
1034 eprintln!(
1035 " Strategy: {:?}, Mutation: {:?}, Crossover: CR={:.3}",
1036 self.config.strategy, self.config.mutation, self.config.recombination
1037 );
1038 eprintln!(
1039 " Tolerances: tol={:.2e}, atol={:.2e}",
1040 self.config.tol, self.config.atol
1041 );
1042 }
1043
1044 let timing_enabled = std::env::var("AUTOEQ_DE_TIMING")
1046 .map(|v| v != "0")
1047 .unwrap_or(false);
1048
1049 if let Some(n) = self.config.parallel.num_threads {
1051 let _ = rayon::ThreadPoolBuilder::new()
1053 .num_threads(n)
1054 .build_global();
1055 }
1056
1057 let mut rng: StdRng = match self.config.seed {
1059 Some(s) => StdRng::seed_from_u64(s),
1060 None => {
1061 let mut thread_rng = rand::rng();
1062 StdRng::from_rng(&mut thread_rng)
1063 }
1064 };
1065
1066 let mut pop = match self.config.init {
1068 Init::LatinHypercube => {
1069 if self.config.disp {
1070 eprintln!(" Using Latin Hypercube initialization");
1071 }
1072 init_latin_hypercube(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1073 }
1074 Init::Random => {
1075 if self.config.disp {
1076 eprintln!(" Using Random initialization");
1077 }
1078 init_random(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1079 }
1080 };
1081
1082 let mut nfev: usize = 0;
1084 if self.config.disp {
1085 eprintln!(" Evaluating initial population of {} individuals...", npop);
1086 }
1087
1088 let mut eval_pop = pop.clone();
1090 let t_integrality0 = Instant::now();
1091 if let Some(mask) = &self.config.integrality {
1092 for i in 0..npop {
1093 let mut row = eval_pop.row_mut(i);
1094 let mut x_eval = row.to_owned();
1095 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1096 row.assign(&x_eval);
1097 }
1098 }
1099 let t_integrality = t_integrality0.elapsed();
1100
1101 let func_ref = self.func;
1103 let penalty_ineq_vec: Vec<PenaltyTuple> = self
1104 .config
1105 .penalty_ineq
1106 .iter()
1107 .map(|(f, w)| (f.clone(), *w))
1108 .collect();
1109 let penalty_eq_vec: Vec<PenaltyTuple> = self
1110 .config
1111 .penalty_eq
1112 .iter()
1113 .map(|(f, w)| (f.clone(), *w))
1114 .collect();
1115 let linear_penalty = self.config.linear_penalty.clone();
1116
1117 let energy_fn = Arc::new(move |x: &Array1<f64>| -> f64 {
1118 let base = (func_ref)(x);
1119 let mut p = 0.0;
1120 for (f, w) in &penalty_ineq_vec {
1121 let v = f(x);
1122 let viol = v.max(0.0);
1123 p += w * viol * viol;
1124 }
1125 for (h, w) in &penalty_eq_vec {
1126 let v = h(x);
1127 p += w * v * v;
1128 }
1129 if let Some(ref lp) = linear_penalty {
1130 let ax = matvec(&lp.a, &x.to_owned());
1131 Zip::from(&ax)
1132 .and(&lp.lb)
1133 .and(&lp.ub)
1134 .for_each(|&v, &lo, &hi| {
1135 if v < lo {
1136 let d = lo - v;
1137 p += lp.weight * d * d;
1138 } else if v > hi {
1139 let d = v - hi;
1140 p += lp.weight * d * d;
1141 }
1142 });
1143 }
1144 base + p
1145 });
1146
1147 let t_eval0 = Instant::now();
1148 let mut energies = parallel_eval::evaluate_population_parallel(
1149 &eval_pop,
1150 energy_fn.clone(),
1151 &self.config.parallel,
1152 );
1153 let t_eval_init = t_eval0.elapsed();
1154 nfev += npop;
1155 if timing_enabled {
1156 eprintln!(
1157 "TIMING init: integrality={:.3} ms, eval={:.3} ms",
1158 t_integrality.as_secs_f64() * 1e3,
1159 t_eval_init.as_secs_f64() * 1e3
1160 );
1161 }
1162
1163 let pop_mean = energies.mean().unwrap_or(0.0);
1165 let pop_std = energies.std(0.0);
1166 if self.config.disp {
1167 eprintln!(
1168 " Initial population: mean={:.6e}, std={:.6e}",
1169 pop_mean, pop_std
1170 );
1171 }
1172
1173 if let Some(x0) = &self.config.x0 {
1175 let mut x0c = x0.clone();
1176 for i in 0..x0c.len() {
1178 x0c[i] = x0c[i].clamp(self.lower[i], self.upper[i]);
1179 }
1180 if let Some(mask) = &self.config.integrality {
1181 apply_integrality(&mut x0c, mask, &self.lower, &self.upper);
1182 }
1183 let f0 = self.energy(&x0c);
1184 nfev += 1;
1185 let (best_idx, _best_f) = argmin(&energies);
1187 pop.row_mut(best_idx).assign(&x0c.view());
1188 energies[best_idx] = f0;
1189 }
1190
1191 let (mut best_idx, mut best_f) = argmin(&energies);
1192 let mut best_x = pop.row(best_idx).to_owned();
1193
1194 if self.config.disp {
1195 eprintln!(
1196 " Initial best: fitness={:.6e} at index {}",
1197 best_f, best_idx
1198 );
1199 let param_summary: Vec<String> = (0..best_x.len() / 3)
1200 .map(|i| {
1201 let freq = 10f64.powf(best_x[i * 3]);
1202 let q = best_x[i * 3 + 1];
1203 let gain = best_x[i * 3 + 2];
1204 format!("f{:.0}Hz/Q{:.2}/G{:.2}dB", freq, q, gain)
1205 })
1206 .collect();
1207 eprintln!(" Initial best params: [{}]", param_summary.join(", "));
1208 }
1209
1210 if self.config.disp {
1211 eprintln!("DE iter {:4} best_f={:.6e}", 0, best_f);
1212 }
1213
1214 let mut adaptive_state = if matches!(
1216 self.config.strategy,
1217 Strategy::AdaptiveBin | Strategy::AdaptiveExp
1218 ) || self.config.adaptive.adaptive_mutation
1219 {
1220 Some(AdaptiveState::new(&self.config.adaptive))
1221 } else {
1222 None
1223 };
1224
1225 let external_archive: Option<Arc<RwLock<external_archive::ExternalArchive>>> = if matches!(
1227 self.config.strategy,
1228 Strategy::LShadeBin | Strategy::LShadeExp
1229 ) {
1230 Some(Arc::new(RwLock::new(
1231 external_archive::ExternalArchive::with_population_size(
1232 npop,
1233 self.config.lshade.arc_rate,
1234 ),
1235 )))
1236 } else {
1237 None
1238 };
1239
1240 let mut success = false;
1242 let mut message = String::new();
1243 let mut nit = 0;
1244 let mut accepted_trials;
1245 let mut improvement_count;
1246
1247 let mut t_build_tot = std::time::Duration::ZERO;
1248 let mut t_eval_tot = std::time::Duration::ZERO;
1249 let mut t_select_tot = std::time::Duration::ZERO;
1250 let mut t_iter_tot = std::time::Duration::ZERO;
1251
1252 for iter in 1..=self.config.maxiter {
1253 nit = iter;
1254 accepted_trials = 0;
1255 improvement_count = 0;
1256
1257 let iter_start = Instant::now();
1258
1259 let sorted_indices = if matches!(
1261 self.config.strategy,
1262 Strategy::AdaptiveBin
1263 | Strategy::AdaptiveExp
1264 | Strategy::LShadeBin
1265 | Strategy::LShadeExp
1266 ) {
1267 let mut indices: Vec<usize> = (0..npop).collect();
1268 indices.sort_by(|&a, &b| {
1269 energies[a]
1270 .partial_cmp(&energies[b])
1271 .unwrap_or(std::cmp::Ordering::Equal)
1272 });
1273 indices
1274 } else {
1275 Vec::new() };
1277
1278 let t_build0 = Instant::now();
1280
1281 use rayon::prelude::*;
1283 let (trials, trial_params): (Vec<_>, Vec<_>) = (0..npop)
1284 .into_par_iter()
1285 .map(|i| {
1286 let mut local_rng: StdRng = if let Some(base_seed) = self.config.seed {
1288 StdRng::seed_from_u64(
1289 base_seed
1290 .wrapping_add((iter as u64) << 32)
1291 .wrapping_add(i as u64),
1292 )
1293 } else {
1294 let mut thread_rng = rand::rng();
1296 StdRng::from_rng(&mut thread_rng)
1297 };
1298
1299 let (f, cr) = if let Some(ref adaptive) = adaptive_state {
1301 let adaptive_f = adaptive.sample_f(&mut local_rng);
1303 let adaptive_cr = adaptive.sample_cr(&mut local_rng);
1304 (adaptive_f, adaptive_cr)
1305 } else {
1306 (
1308 self.config.mutation.sample(&mut local_rng),
1309 self.config.recombination,
1310 )
1311 };
1312
1313 let (mutant, cross) = match self.config.strategy {
1315 Strategy::Best1Bin => (
1316 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1317 Crossover::Binomial,
1318 ),
1319 Strategy::Best1Exp => (
1320 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1321 Crossover::Exponential,
1322 ),
1323 Strategy::Rand1Bin => (
1324 mutant_rand1(i, &pop, f, &mut local_rng),
1325 Crossover::Binomial,
1326 ),
1327 Strategy::Rand1Exp => (
1328 mutant_rand1(i, &pop, f, &mut local_rng),
1329 Crossover::Exponential,
1330 ),
1331 Strategy::Rand2Bin => (
1332 mutant_rand2(i, &pop, f, &mut local_rng),
1333 Crossover::Binomial,
1334 ),
1335 Strategy::Rand2Exp => (
1336 mutant_rand2(i, &pop, f, &mut local_rng),
1337 Crossover::Exponential,
1338 ),
1339 Strategy::CurrentToBest1Bin => (
1340 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1341 Crossover::Binomial,
1342 ),
1343 Strategy::CurrentToBest1Exp => (
1344 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1345 Crossover::Exponential,
1346 ),
1347 Strategy::Best2Bin => (
1348 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1349 Crossover::Binomial,
1350 ),
1351 Strategy::Best2Exp => (
1352 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1353 Crossover::Exponential,
1354 ),
1355 Strategy::RandToBest1Bin => (
1356 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1357 Crossover::Binomial,
1358 ),
1359 Strategy::RandToBest1Exp => (
1360 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1361 Crossover::Exponential,
1362 ),
1363 Strategy::AdaptiveBin => {
1364 if let Some(ref adaptive) = adaptive_state {
1365 (
1366 mutant_adaptive(
1367 i,
1368 &pop,
1369 &sorted_indices,
1370 adaptive.current_w,
1371 f,
1372 &mut local_rng,
1373 ),
1374 Crossover::Binomial,
1375 )
1376 } else {
1377 (
1379 mutant_rand1(i, &pop, f, &mut local_rng),
1380 Crossover::Binomial,
1381 )
1382 }
1383 }
1384 Strategy::AdaptiveExp => {
1385 if let Some(ref adaptive) = adaptive_state {
1386 (
1387 mutant_adaptive(
1388 i,
1389 &pop,
1390 &sorted_indices,
1391 adaptive.current_w,
1392 f,
1393 &mut local_rng,
1394 ),
1395 Crossover::Exponential,
1396 )
1397 } else {
1398 (
1400 mutant_rand1(i, &pop, f, &mut local_rng),
1401 Crossover::Exponential,
1402 )
1403 }
1404 }
1405 Strategy::LShadeBin => {
1406 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1407 self.config.lshade.p,
1408 pop.nrows(),
1409 );
1410 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1411 (
1412 mutant_current_to_pbest1::mutant_current_to_pbest1(
1413 i,
1414 &pop,
1415 &sorted_indices,
1416 pbest_size,
1417 archive_ref.as_deref(),
1418 f,
1419 &mut local_rng,
1420 ),
1421 Crossover::Binomial,
1422 )
1423 }
1424 Strategy::LShadeExp => {
1425 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1426 self.config.lshade.p,
1427 pop.nrows(),
1428 );
1429 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1430 (
1431 mutant_current_to_pbest1::mutant_current_to_pbest1(
1432 i,
1433 &pop,
1434 &sorted_indices,
1435 pbest_size,
1436 archive_ref.as_deref(),
1437 f,
1438 &mut local_rng,
1439 ),
1440 Crossover::Exponential,
1441 )
1442 }
1443 };
1444
1445 let crossover = cross;
1447 let trial = match crossover {
1448 Crossover::Binomial => {
1449 binomial_crossover(&pop.row(i).to_owned(), &mutant, cr, &mut local_rng)
1450 }
1451 Crossover::Exponential => exponential_crossover(
1452 &pop.row(i).to_owned(),
1453 &mutant,
1454 cr,
1455 &mut local_rng,
1456 ),
1457 };
1458
1459 let wls_trial = if self.config.adaptive.wls_enabled
1461 && local_rng.random::<f64>() < self.config.adaptive.wls_prob
1462 {
1463 apply_wls(
1464 &trial,
1465 &self.lower,
1466 &self.upper,
1467 self.config.adaptive.wls_scale,
1468 &mut local_rng,
1469 )
1470 } else {
1471 trial.clone()
1472 };
1473
1474 let mut trial_clipped = wls_trial;
1476 Zip::from(&mut trial_clipped)
1477 .and(&self.lower)
1478 .and(&self.upper)
1479 .for_each(|x, lo, hi| *x = x.clamp(*lo, *hi));
1480
1481 if let Some(mask) = &self.config.integrality {
1483 apply_integrality(&mut trial_clipped, mask, &self.lower, &self.upper);
1484 }
1485
1486 (trial_clipped, (f, cr))
1488 })
1489 .unzip();
1490
1491 let t_build = t_build0.elapsed();
1492 let t_eval0 = Instant::now();
1493 let trial_energies =
1494 evaluate_trials_parallel(&trials, energy_fn.clone(), &self.config.parallel);
1495 let t_eval = t_eval0.elapsed();
1496 nfev += npop;
1497
1498 let t_select0 = Instant::now();
1499 for (i, (trial, trial_energy)) in
1501 trials.into_iter().zip(trial_energies.iter()).enumerate()
1502 {
1503 let (f, cr) = trial_params[i];
1504
1505 if *trial_energy <= energies[i] {
1507 if let Some(ref archive) = external_archive
1509 && *trial_energy < energies[i]
1510 && let Ok(mut arch) = archive.write()
1511 {
1512 arch.add(pop.row(i).to_owned());
1513 }
1514 pop.row_mut(i).assign(&trial.view());
1515 energies[i] = *trial_energy;
1516 accepted_trials += 1;
1517
1518 if let Some(ref mut adaptive) = adaptive_state {
1520 adaptive.record_success(f, cr);
1521 }
1522
1523 if *trial_energy < best_f {
1525 improvement_count += 1;
1526 }
1527 }
1528 }
1529 let t_select = t_select0.elapsed();
1530
1531 t_build_tot += t_build;
1532 t_eval_tot += t_eval;
1533 t_select_tot += t_select;
1534 let iter_dur = iter_start.elapsed();
1535 t_iter_tot += iter_dur;
1536
1537 if timing_enabled && (iter <= 5 || iter % 10 == 0) {
1538 eprintln!(
1539 "TIMING iter {:4}: build={:.3} ms, eval={:.3} ms, select={:.3} ms, total={:.3} ms",
1540 iter,
1541 t_build.as_secs_f64() * 1e3,
1542 t_eval.as_secs_f64() * 1e3,
1543 t_select.as_secs_f64() * 1e3,
1544 iter_dur.as_secs_f64() * 1e3,
1545 );
1546 }
1547
1548 if let Some(ref mut adaptive) = adaptive_state {
1550 adaptive.update(&self.config.adaptive, iter, self.config.maxiter);
1551 }
1552
1553 let (new_best_idx, new_best_f) = argmin(&energies);
1555 if new_best_f < best_f {
1556 best_idx = new_best_idx;
1557 best_f = new_best_f;
1558 best_x = pop.row(best_idx).to_owned();
1559 }
1560
1561 let pop_mean = energies.mean().unwrap_or(0.0);
1563 let pop_std = energies.std(0.0);
1564 let convergence_threshold = self.config.atol + self.config.tol * pop_mean.abs();
1565
1566 if self.config.disp {
1567 eprintln!(
1568 "DE iter {:4} best_f={:.6e} std={:.3e} accepted={}/{}, improved={}",
1569 iter, best_f, pop_std, accepted_trials, npop, improvement_count
1570 );
1571 }
1572
1573 if let Some(ref mut cb) = self.config.callback {
1575 let intermediate = DEIntermediate {
1576 x: best_x.clone(),
1577 fun: best_f,
1578 convergence: pop_std,
1579 iter,
1580 };
1581 match cb(&intermediate) {
1582 CallbackAction::Stop => {
1583 success = true;
1584 message = "Optimization stopped by callback".to_string();
1585 break;
1586 }
1587 CallbackAction::Continue => {}
1588 }
1589 }
1590
1591 if pop_std <= convergence_threshold {
1592 success = true;
1593 message = format!(
1594 "Converged: std(pop_f)={:.3e} <= threshold={:.3e}",
1595 pop_std, convergence_threshold
1596 );
1597 break;
1598 }
1599 }
1600
1601 if !success {
1602 message = format!("Maximum iterations reached: {}", self.config.maxiter);
1603 }
1604
1605 if self.config.disp {
1606 eprintln!("DE finished: {}", message);
1607 }
1608
1609 let (final_x, final_f, polish_nfev) = if let Some(ref polish_cfg) = self.config.polish {
1611 if polish_cfg.enabled {
1612 self.polish(&best_x)
1613 } else {
1614 (best_x.clone(), best_f, 0)
1615 }
1616 } else {
1617 (best_x.clone(), best_f, 0)
1618 };
1619
1620 if timing_enabled {
1621 eprintln!(
1622 "TIMING total: build={:.3} s, eval={:.3} s, select={:.3} s, iter_total={:.3} s",
1623 t_build_tot.as_secs_f64(),
1624 t_eval_tot.as_secs_f64(),
1625 t_select_tot.as_secs_f64(),
1626 t_iter_tot.as_secs_f64()
1627 );
1628 }
1629
1630 self.finish_report(
1631 pop,
1632 energies,
1633 final_x,
1634 final_f,
1635 success,
1636 message,
1637 nit,
1638 nfev + polish_nfev,
1639 )
1640 }
1641}
1642
1643#[cfg(test)]
1644mod strategy_tests {
1645 use super::*;
1646
1647 #[test]
1648 fn test_parse_strategy_variants() {
1649 assert!(matches!(
1650 "best1exp".parse::<Strategy>().unwrap(),
1651 Strategy::Best1Exp
1652 ));
1653 assert!(matches!(
1654 "rand1bin".parse::<Strategy>().unwrap(),
1655 Strategy::Rand1Bin
1656 ));
1657 assert!(matches!(
1658 "randtobest1exp".parse::<Strategy>().unwrap(),
1659 Strategy::RandToBest1Exp
1660 ));
1661 }
1662}