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
83pub mod cobyla;
89mod cobyla_native;
90pub use cobyla::{CobylaConfig, CobylaConstraint, CobylaReport, CobylaStopTols};
91
92pub mod isres;
94pub use isres::{IsresConfig, IsresConstraint, IsresReport, isres};
95
96use std::fmt;
97use std::str::FromStr;
98use std::sync::Arc;
99use std::sync::RwLock;
100
101use ndarray::{Array1, Array2, Zip};
102use oxiblas_ndarray::blas::matvec;
103use rand::rngs::StdRng;
104use rand::{Rng, SeedableRng};
105use std::time::Instant;
106
107pub mod stack_linear_penalty;
109
110pub mod apply_integrality;
112pub mod apply_wls;
114
115pub mod distinct_indices;
117pub mod external_archive;
119pub mod init_latin_hypercube;
121pub mod init_random;
123pub mod init_sobol;
125pub mod lshade;
127
128pub mod mutant_adaptive;
130pub mod mutant_best1;
132pub mod mutant_best2;
134pub mod mutant_current_to_best1;
136pub mod mutant_current_to_pbest1;
138pub mod mutant_rand1;
140pub mod mutant_rand2;
142pub mod mutant_rand_to_best1;
144
145pub mod crossover_binomial;
147pub mod crossover_exponential;
149
150#[cfg(test)]
152mod de_tests;
153pub mod differential_evolution;
155pub mod function_registry;
157pub mod impl_helpers;
159pub mod metadata;
161pub mod parallel_eval;
163pub mod recorder;
165pub mod run_recorded;
167pub use differential_evolution::differential_evolution;
168pub use external_archive::ExternalArchive;
169pub use lshade::LShadeConfig;
170pub use parallel_eval::ParallelConfig;
171pub use recorder::{OptimizationRecord, OptimizationRecorder};
172pub use run_recorded::run_recorded_differential_evolution;
173
174pub type ScalarConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
177pub type VectorConstraintFn = Arc<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
179pub type PenaltyTuple = (ScalarConstraintFn, f64);
181pub type CallbackFn = Box<dyn FnMut(&DEIntermediate) -> CallbackAction>;
183
184pub(crate) fn argmin(v: &Array1<f64>) -> (usize, f64) {
185 let mut best_i = 0usize;
186 let mut best_v = v[0];
187 for (i, &val) in v.iter().enumerate() {
188 if val < best_v {
189 best_v = val;
190 best_i = i;
191 }
192 }
193 (best_i, best_v)
194}
195
196#[derive(Debug, Clone, Copy)]
203pub enum Strategy {
204 Best1Bin,
206 Best1Exp,
208 Rand1Bin,
210 Rand1Exp,
212 Rand2Bin,
214 Rand2Exp,
216 CurrentToBest1Bin,
218 CurrentToBest1Exp,
220 Best2Bin,
222 Best2Exp,
224 RandToBest1Bin,
226 RandToBest1Exp,
228 AdaptiveBin,
230 AdaptiveExp,
232 LShadeBin,
234 LShadeExp,
236}
237
238impl FromStr for Strategy {
239 type Err = String;
240 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
241 let t = s.to_lowercase();
242 match t.as_str() {
243 "best1bin" | "best1" => Ok(Strategy::Best1Bin),
244 "best1exp" => Ok(Strategy::Best1Exp),
245 "rand1bin" | "rand1" => Ok(Strategy::Rand1Bin),
246 "rand1exp" => Ok(Strategy::Rand1Exp),
247 "rand2bin" | "rand2" => Ok(Strategy::Rand2Bin),
248 "rand2exp" => Ok(Strategy::Rand2Exp),
249 "currenttobest1bin" | "current-to-best1bin" | "current_to_best1bin" => {
250 Ok(Strategy::CurrentToBest1Bin)
251 }
252 "currenttobest1exp" | "current-to-best1exp" | "current_to_best1exp" => {
253 Ok(Strategy::CurrentToBest1Exp)
254 }
255 "best2bin" | "best2" => Ok(Strategy::Best2Bin),
256 "best2exp" => Ok(Strategy::Best2Exp),
257 "randtobest1bin" | "rand-to-best1bin" | "rand_to_best1bin" => {
258 Ok(Strategy::RandToBest1Bin)
259 }
260 "randtobest1exp" | "rand-to-best1exp" | "rand_to_best1exp" => {
261 Ok(Strategy::RandToBest1Exp)
262 }
263 "adaptivebin" | "adaptive-bin" | "adaptive_bin" | "adaptive" => {
264 Ok(Strategy::AdaptiveBin)
265 }
266 "adaptiveexp" | "adaptive-exp" | "adaptive_exp" => Ok(Strategy::AdaptiveExp),
267 "lshadebin" | "lshade-bin" | "lshade_bin" | "lshade" => Ok(Strategy::LShadeBin),
268 "lshadeexp" | "lshade-exp" | "lshade_exp" => Ok(Strategy::LShadeExp),
269 _ => Err(format!("unknown strategy: {}", s)),
270 }
271 }
272}
273
274#[derive(Debug, Clone, Copy, Default)]
276pub enum Crossover {
277 #[default]
279 Binomial,
280 Exponential,
282}
283
284#[derive(Debug, Clone, Copy)]
286pub enum Mutation {
287 Factor(f64),
289 Range {
291 min: f64,
293 max: f64,
295 },
296 Adaptive {
298 initial_f: f64,
300 },
301}
302
303impl Default for Mutation {
304 fn default() -> Self {
305 let _ = Mutation::Factor(0.8);
306 Mutation::Range { min: 0.0, max: 2.0 }
307 }
308}
309
310impl Mutation {
311 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
312 match *self {
313 Mutation::Factor(f) => f,
314 Mutation::Range { min, max } => rng.random_range(min..max),
315 Mutation::Adaptive { initial_f } => initial_f, }
317 }
318
319 #[allow(dead_code)]
321 fn sample_cauchy<R: Rng + ?Sized>(&self, f_m: f64, _scale: f64, rng: &mut R) -> f64 {
322 let perturbation = (rng.random::<f64>() - 0.5) * 0.2; (f_m + perturbation).clamp(0.0, 2.0) }
326}
327
328#[derive(Debug, Clone, Copy, Default)]
330pub enum Init {
331 #[default]
333 LatinHypercube,
334 Random,
336}
337
338#[derive(Debug, Clone, Copy, Default)]
340pub enum Updating {
341 #[default]
343 Deferred,
344}
345
346#[derive(Debug, Clone)]
348pub struct LinearPenalty {
349 pub a: Array2<f64>,
351 pub lb: Array1<f64>,
353 pub ub: Array1<f64>,
355 pub weight: f64,
357}
358
359#[derive(Debug, Clone)]
361pub struct LinearConstraintHelper {
362 pub a: Array2<f64>,
364 pub lb: Array1<f64>,
366 pub ub: Array1<f64>,
368}
369
370impl LinearConstraintHelper {
371 pub fn apply_to(&self, cfg: &mut DEConfig, weight: f64) {
373 use stack_linear_penalty::stack_linear_penalty;
374
375 let new_lp = LinearPenalty {
376 a: self.a.clone(),
377 lb: self.lb.clone(),
378 ub: self.ub.clone(),
379 weight,
380 };
381 match &mut cfg.linear_penalty {
382 Some(existing) => stack_linear_penalty(existing, &new_lp),
383 None => cfg.linear_penalty = Some(new_lp),
384 }
385 }
386}
387
388#[derive(Clone)]
390pub struct NonlinearConstraintHelper {
391 pub fun: VectorConstraintFn,
393 pub lb: Array1<f64>,
395 pub ub: Array1<f64>,
397}
398
399impl NonlinearConstraintHelper {
400 pub fn apply_to(&self, cfg: &mut DEConfig, weight_ineq: f64, weight_eq: f64) {
404 let f = self.fun.clone();
405 let lb = self.lb.clone();
406 let ub = self.ub.clone();
407 let m = lb.len().min(ub.len());
408 for i in 0..m {
409 let l = lb[i];
410 let u = ub[i];
411 let tol = 1e-12 * (l.abs() + u.abs()).max(1.0);
412 if (u - l).abs() <= tol {
413 let fi = f.clone();
414 cfg.penalty_eq.push((
415 Arc::new(move |x: &Array1<f64>| {
416 let y = (fi)(x);
417 y[i] - l
418 }),
419 weight_eq,
420 ));
421 } else {
422 let fi_u = f.clone();
423 cfg.penalty_ineq.push((
424 Arc::new(move |x: &Array1<f64>| {
425 let y = (fi_u)(x);
426 y[i] - u
427 }),
428 weight_ineq,
429 ));
430 let fi_l = f.clone();
431 cfg.penalty_ineq.push((
432 Arc::new(move |x: &Array1<f64>| {
433 let y = (fi_l)(x);
434 l - y[i]
435 }),
436 weight_ineq,
437 ));
438 }
439 }
440 }
441}
442
443#[derive(Debug, Clone)]
445struct AdaptiveState {
446 f_m: f64,
448 cr_m: f64,
450 successful_f: Vec<f64>,
452 successful_cr: Vec<f64>,
454 current_w: f64,
456}
457
458impl AdaptiveState {
459 fn new(config: &AdaptiveConfig) -> Self {
460 Self {
461 f_m: config.f_m,
462 cr_m: config.cr_m,
463 successful_f: Vec::new(),
464 successful_cr: Vec::new(),
465 current_w: config.w_max, }
467 }
468
469 fn update(&mut self, config: &AdaptiveConfig, iter: usize, max_iter: usize) {
471 let iter_ratio = iter as f64 / max_iter as f64;
473 self.current_w = config.w_max - (config.w_max - config.w_min) * iter_ratio;
474
475 if !self.successful_f.is_empty() {
477 let mean_f = self.compute_lehmer_mean(&self.successful_f);
478 self.f_m = (1.0 - config.w_f) * self.f_m + config.w_f * mean_f;
479 self.f_m = self.f_m.clamp(0.2, 1.2);
480 }
481
482 if !self.successful_cr.is_empty() {
484 let mean_cr = self.compute_arithmetic_mean(&self.successful_cr);
485 self.cr_m = (1.0 - config.w_cr) * self.cr_m + config.w_cr * mean_cr;
486 self.cr_m = self.cr_m.clamp(0.1, 0.9);
487 }
488
489 self.successful_f.clear();
491 self.successful_cr.clear();
492 }
493
494 fn compute_lehmer_mean(&self, values: &[f64]) -> f64 {
496 if values.is_empty() {
497 return 0.5; }
499
500 let sum_sq: f64 = values.iter().map(|&x| x * x).sum();
501 let sum: f64 = values.iter().sum();
502
503 if sum > 0.0 {
504 sum_sq / sum
505 } else {
506 0.5 }
508 }
509
510 fn compute_arithmetic_mean(&self, values: &[f64]) -> f64 {
512 if values.is_empty() {
513 return 0.5; }
515 values.iter().sum::<f64>() / values.len() as f64
516 }
517
518 fn record_success(&mut self, f_val: f64, cr_val: f64) {
520 self.successful_f.push(f_val);
521 self.successful_cr.push(cr_val);
522 }
523
524 fn sample_f<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
526 let u1: f64 = rng.random::<f64>().max(1e-15);
527 let u2: f64 = rng.random::<f64>();
528
529 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
530 let sample = self.f_m + 0.05 * normal;
531
532 sample.clamp(0.3, 1.0)
533 }
534
535 fn sample_cr<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
537 let u1: f64 = rng.random::<f64>().max(1e-15);
538 let u2: f64 = rng.random::<f64>();
539
540 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
541 let sample = self.cr_m + 0.05 * normal;
542
543 sample.clamp(0.1, 0.9)
544 }
545}
546
547#[derive(Debug, Clone)]
549pub struct AdaptiveConfig {
550 pub adaptive_mutation: bool,
552 pub wls_enabled: bool,
554 pub w_max: f64,
556 pub w_min: f64,
558 pub w_f: f64,
560 pub w_cr: f64,
562 pub f_m: f64,
564 pub cr_m: f64,
566 pub wls_prob: f64,
568 pub wls_scale: f64,
570}
571
572impl Default for AdaptiveConfig {
573 fn default() -> Self {
574 Self {
575 adaptive_mutation: false,
576 wls_enabled: false,
577 w_max: 0.9,
578 w_min: 0.1,
579 w_f: 0.9,
580 w_cr: 0.9,
581 f_m: 0.5,
582 cr_m: 0.6,
583 wls_prob: 0.1,
584 wls_scale: 0.1,
585 }
586 }
587}
588
589#[derive(Debug, Clone)]
591pub struct PolishConfig {
592 pub enabled: bool,
594 pub algo: String,
596 pub maxeval: usize,
598}
599
600pub struct DEConfig {
606 pub maxiter: usize,
608 pub popsize: usize,
610 pub tol: f64,
612 pub atol: f64,
614 pub mutation: Mutation,
616 pub recombination: f64,
618 pub strategy: Strategy,
620 pub crossover: Crossover,
622 pub init: Init,
624 pub updating: Updating,
626 pub seed: Option<u64>,
628 pub integrality: Option<Vec<bool>>,
630 pub x0: Option<Array1<f64>>,
632 pub disp: bool,
634 pub callback: Option<CallbackFn>,
636 pub penalty_ineq: Vec<PenaltyTuple>,
638 pub penalty_eq: Vec<PenaltyTuple>,
640 pub linear_penalty: Option<LinearPenalty>,
642 pub polish: Option<PolishConfig>,
644 pub adaptive: AdaptiveConfig,
646 pub parallel: parallel_eval::ParallelConfig,
648 pub lshade: lshade::LShadeConfig,
650}
651
652impl Default for DEConfig {
653 fn default() -> Self {
654 Self {
655 maxiter: 1000,
656 popsize: 15,
657 tol: 1e-2,
658 atol: 0.0,
659 mutation: Mutation::default(),
660 recombination: 0.7,
661 strategy: Strategy::Best1Bin,
662 crossover: Crossover::default(),
663 init: Init::default(),
664 updating: Updating::default(),
665 seed: None,
666 integrality: None,
667 x0: None,
668 disp: false,
669 callback: None,
670 penalty_ineq: Vec::new(),
671 penalty_eq: Vec::new(),
672 linear_penalty: None,
673 polish: None,
674 adaptive: AdaptiveConfig::default(),
675 parallel: parallel_eval::ParallelConfig::default(),
676 lshade: lshade::LShadeConfig::default(),
677 }
678 }
679}
680
681pub struct DEConfigBuilder {
698 cfg: DEConfig,
699}
700impl Default for DEConfigBuilder {
701 fn default() -> Self {
702 Self::new()
703 }
704}
705
706impl DEConfigBuilder {
707 pub fn new() -> Self {
709 Self {
710 cfg: DEConfig::default(),
711 }
712 }
713 pub fn maxiter(mut self, v: usize) -> Self {
715 self.cfg.maxiter = v;
716 self
717 }
718 pub fn popsize(mut self, v: usize) -> Self {
725 self.cfg.popsize = v;
726 self
727 }
728 pub fn tol(mut self, v: f64) -> Self {
730 self.cfg.tol = v;
731 self
732 }
733 pub fn atol(mut self, v: f64) -> Self {
735 self.cfg.atol = v;
736 self
737 }
738 pub fn mutation(mut self, v: Mutation) -> Self {
740 self.cfg.mutation = v;
741 self
742 }
743 pub fn recombination(mut self, v: f64) -> Self {
745 self.cfg.recombination = v;
746 self
747 }
748 pub fn strategy(mut self, v: Strategy) -> Self {
750 self.cfg.strategy = v;
751 self
752 }
753 pub fn crossover(mut self, v: Crossover) -> Self {
755 self.cfg.crossover = v;
756 self
757 }
758 pub fn init(mut self, v: Init) -> Self {
760 self.cfg.init = v;
761 self
762 }
763 pub fn seed(mut self, v: u64) -> Self {
765 self.cfg.seed = Some(v);
766 self
767 }
768 pub fn integrality(mut self, v: Vec<bool>) -> Self {
770 self.cfg.integrality = Some(v);
771 self
772 }
773 pub fn x0(mut self, v: Array1<f64>) -> Self {
775 self.cfg.x0 = Some(v);
776 self
777 }
778 pub fn disp(mut self, v: bool) -> Self {
780 self.cfg.disp = v;
781 self
782 }
783 pub fn callback(mut self, cb: Box<dyn FnMut(&DEIntermediate) -> CallbackAction>) -> Self {
785 self.cfg.callback = Some(cb);
786 self
787 }
788 pub fn add_penalty_ineq<FN>(mut self, f: FN, w: f64) -> Self
790 where
791 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
792 {
793 self.cfg.penalty_ineq.push((Arc::new(f), w));
794 self
795 }
796 pub fn add_penalty_eq<FN>(mut self, f: FN, w: f64) -> Self
798 where
799 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
800 {
801 self.cfg.penalty_eq.push((Arc::new(f), w));
802 self
803 }
804 pub fn linear_penalty(mut self, lp: LinearPenalty) -> Self {
806 self.cfg.linear_penalty = Some(lp);
807 self
808 }
809 pub fn polish(mut self, pol: PolishConfig) -> Self {
811 self.cfg.polish = Some(pol);
812 self
813 }
814 pub fn adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
816 self.cfg.adaptive = adaptive;
817 self
818 }
819 pub fn enable_adaptive_mutation(mut self, enable: bool) -> Self {
821 self.cfg.adaptive.adaptive_mutation = enable;
822 self
823 }
824 pub fn enable_wls(mut self, enable: bool) -> Self {
826 self.cfg.adaptive.wls_enabled = enable;
827 self
828 }
829 pub fn adaptive_weights(mut self, w_max: f64, w_min: f64) -> Self {
831 self.cfg.adaptive.w_max = w_max;
832 self.cfg.adaptive.w_min = w_min;
833 self
834 }
835 pub fn parallel(mut self, parallel: parallel_eval::ParallelConfig) -> Self {
837 self.cfg.parallel = parallel;
838 self
839 }
840 pub fn enable_parallel(mut self, enable: bool) -> Self {
842 self.cfg.parallel.enabled = enable;
843 self
844 }
845 pub fn parallel_threads(mut self, num_threads: usize) -> Self {
847 self.cfg.parallel.num_threads = Some(num_threads);
848 self
849 }
850 pub fn lshade(mut self, lshade: lshade::LShadeConfig) -> Self {
852 self.cfg.lshade = lshade;
853 self
854 }
855 pub fn build(self) -> error::Result<DEConfig> {
861 if self.cfg.popsize < 4 {
862 return Err(DEError::PopulationTooSmall {
863 pop_size: self.cfg.popsize,
864 });
865 }
866 Ok(self.cfg)
867 }
868}
869
870#[derive(Clone)]
874pub struct DEReport {
875 pub x: Array1<f64>,
877 pub fun: f64,
879 pub success: bool,
881 pub message: String,
883 pub nit: usize,
885 pub nfev: usize,
887 pub population: Array2<f64>,
889 pub population_energies: Array1<f64>,
891}
892
893impl fmt::Debug for DEReport {
894 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
895 f.debug_struct("DEReport")
896 .field("x", &format!("len={}", self.x.len()))
897 .field("fun", &self.fun)
898 .field("success", &self.success)
899 .field("message", &self.message)
900 .field("nit", &self.nit)
901 .field("nfev", &self.nfev)
902 .field(
903 "population",
904 &format!("{}x{}", self.population.nrows(), self.population.ncols()),
905 )
906 .field(
907 "population_energies",
908 &format!("len={}", self.population_energies.len()),
909 )
910 .finish()
911 }
912}
913
914pub struct DEIntermediate {
916 pub x: Array1<f64>,
918 pub fun: f64,
920 pub convergence: f64,
922 pub iter: usize,
924}
925
926pub enum CallbackAction {
928 Continue,
930 Stop,
932}
933
934pub struct DifferentialEvolution<'a, F>
940where
941 F: Fn(&Array1<f64>) -> f64 + Sync,
942{
943 func: &'a F,
944 lower: Array1<f64>,
945 upper: Array1<f64>,
946 config: DEConfig,
947}
948
949impl<'a, F> DifferentialEvolution<'a, F>
950where
951 F: Fn(&Array1<f64>) -> f64 + Sync,
952{
953 pub fn new(func: &'a F, lower: Array1<f64>, upper: Array1<f64>) -> Result<Self> {
960 if lower.len() != upper.len() {
961 return Err(DEError::BoundsMismatch {
962 lower_len: lower.len(),
963 upper_len: upper.len(),
964 });
965 }
966
967 for i in 0..lower.len() {
969 if lower[i] > upper[i] {
970 return Err(DEError::InvalidBounds {
971 index: i,
972 lower: lower[i],
973 upper: upper[i],
974 });
975 }
976 }
977
978 Ok(Self {
979 func,
980 lower,
981 upper,
982 config: DEConfig::default(),
983 })
984 }
985
986 pub fn config_mut(&mut self) -> &mut DEConfig {
988 &mut self.config
989 }
990
991 pub fn solve(&mut self) -> DEReport {
993 use apply_integrality::apply_integrality;
994 use apply_wls::apply_wls;
995 use crossover_binomial::binomial_crossover;
996 use crossover_exponential::exponential_crossover;
997 use init_latin_hypercube::init_latin_hypercube;
998 use init_random::init_random;
999 use mutant_adaptive::mutant_adaptive;
1000 use mutant_best1::mutant_best1;
1001 use mutant_best2::mutant_best2;
1002 use mutant_current_to_best1::mutant_current_to_best1;
1003 use mutant_current_to_pbest1;
1004 use mutant_rand_to_best1::mutant_rand_to_best1;
1005 use mutant_rand1::mutant_rand1;
1006 use mutant_rand2::mutant_rand2;
1007 use parallel_eval::evaluate_trials_parallel;
1008 use std::sync::Arc;
1009
1010 let n = self.lower.len();
1011
1012 let mut is_free: Vec<bool> = Vec::with_capacity(n);
1014 for i in 0..n {
1015 is_free.push((self.upper[i] - self.lower[i]).abs() > 0.0);
1016 }
1017 let n_free = is_free.iter().filter(|&&b| b).count();
1018 let _n_equal = n - n_free;
1019 if n_free == 0 {
1020 let x_fixed = self.lower.clone();
1022 let mut x_eval = x_fixed.clone();
1023 if let Some(mask) = &self.config.integrality {
1024 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1025 }
1026 let f = (self.func)(&x_eval);
1027 return DEReport {
1028 x: x_eval,
1029 fun: f,
1030 success: true,
1031 message: "All variables fixed by bounds".into(),
1032 nit: 0,
1033 nfev: 1,
1034 population: Array2::zeros((1, n)),
1035 population_energies: Array1::from(vec![f]),
1036 };
1037 }
1038
1039 let npop = self.config.popsize * n_free;
1040 let _bounds_span = &self.upper - &self.lower;
1041
1042 if self.config.disp {
1043 eprintln!(
1044 "DE Init: {} dimensions ({} free), population={}, maxiter={}",
1045 n, n_free, npop, self.config.maxiter
1046 );
1047 eprintln!(
1048 " Strategy: {:?}, Mutation: {:?}, Crossover: CR={:.3}",
1049 self.config.strategy, self.config.mutation, self.config.recombination
1050 );
1051 eprintln!(
1052 " Tolerances: tol={:.2e}, atol={:.2e}",
1053 self.config.tol, self.config.atol
1054 );
1055 }
1056
1057 let timing_enabled = std::env::var("AUTOEQ_DE_TIMING")
1059 .map(|v| v != "0")
1060 .unwrap_or(false);
1061
1062 if let Some(n) = self.config.parallel.num_threads {
1064 let _ = rayon::ThreadPoolBuilder::new()
1066 .num_threads(n)
1067 .build_global();
1068 }
1069
1070 let mut rng: StdRng = match self.config.seed {
1072 Some(s) => StdRng::seed_from_u64(s),
1073 None => {
1074 let mut thread_rng = rand::rng();
1075 StdRng::from_rng(&mut thread_rng)
1076 }
1077 };
1078
1079 let mut pop = match self.config.init {
1081 Init::LatinHypercube => {
1082 if self.config.disp {
1083 eprintln!(" Using Latin Hypercube initialization");
1084 }
1085 init_latin_hypercube(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1086 }
1087 Init::Random => {
1088 if self.config.disp {
1089 eprintln!(" Using Random initialization");
1090 }
1091 init_random(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1092 }
1093 };
1094
1095 let mut nfev: usize = 0;
1097 if self.config.disp {
1098 eprintln!(" Evaluating initial population of {} individuals...", npop);
1099 }
1100
1101 let mut eval_pop = pop.clone();
1103 let t_integrality0 = Instant::now();
1104 if let Some(mask) = &self.config.integrality {
1105 for i in 0..npop {
1106 let mut row = eval_pop.row_mut(i);
1107 let mut x_eval = row.to_owned();
1108 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1109 row.assign(&x_eval);
1110 }
1111 }
1112 let t_integrality = t_integrality0.elapsed();
1113
1114 let func_ref = self.func;
1116 let penalty_ineq_vec: Vec<PenaltyTuple> = self
1117 .config
1118 .penalty_ineq
1119 .iter()
1120 .map(|(f, w)| (f.clone(), *w))
1121 .collect();
1122 let penalty_eq_vec: Vec<PenaltyTuple> = self
1123 .config
1124 .penalty_eq
1125 .iter()
1126 .map(|(f, w)| (f.clone(), *w))
1127 .collect();
1128 let linear_penalty = self.config.linear_penalty.clone();
1129
1130 let energy_fn = Arc::new(move |x: &Array1<f64>| -> f64 {
1131 let base = (func_ref)(x);
1132 let mut p = 0.0;
1133 for (f, w) in &penalty_ineq_vec {
1134 let v = f(x);
1135 let viol = v.max(0.0);
1136 p += w * viol * viol;
1137 }
1138 for (h, w) in &penalty_eq_vec {
1139 let v = h(x);
1140 p += w * v * v;
1141 }
1142 if let Some(ref lp) = linear_penalty {
1143 let ax = matvec(&lp.a, &x.to_owned());
1144 Zip::from(&ax)
1145 .and(&lp.lb)
1146 .and(&lp.ub)
1147 .for_each(|&v, &lo, &hi| {
1148 if v < lo {
1149 let d = lo - v;
1150 p += lp.weight * d * d;
1151 } else if v > hi {
1152 let d = v - hi;
1153 p += lp.weight * d * d;
1154 }
1155 });
1156 }
1157 base + p
1158 });
1159
1160 let t_eval0 = Instant::now();
1161 let mut energies = parallel_eval::evaluate_population_parallel(
1162 &eval_pop,
1163 energy_fn.clone(),
1164 &self.config.parallel,
1165 );
1166 let t_eval_init = t_eval0.elapsed();
1167 nfev += npop;
1168 if timing_enabled {
1169 eprintln!(
1170 "TIMING init: integrality={:.3} ms, eval={:.3} ms",
1171 t_integrality.as_secs_f64() * 1e3,
1172 t_eval_init.as_secs_f64() * 1e3
1173 );
1174 }
1175
1176 let pop_mean = energies.mean().unwrap_or(0.0);
1178 let pop_std = energies.std(0.0);
1179 if self.config.disp {
1180 eprintln!(
1181 " Initial population: mean={:.6e}, std={:.6e}",
1182 pop_mean, pop_std
1183 );
1184 }
1185
1186 if let Some(x0) = &self.config.x0 {
1188 let mut x0c = x0.clone();
1189 for i in 0..x0c.len() {
1191 x0c[i] = x0c[i].clamp(self.lower[i], self.upper[i]);
1192 }
1193 if let Some(mask) = &self.config.integrality {
1194 apply_integrality(&mut x0c, mask, &self.lower, &self.upper);
1195 }
1196 let f0 = self.energy(&x0c);
1197 nfev += 1;
1198 let (best_idx, _best_f) = argmin(&energies);
1200 pop.row_mut(best_idx).assign(&x0c.view());
1201 energies[best_idx] = f0;
1202 }
1203
1204 let (mut best_idx, mut best_f) = argmin(&energies);
1205 let mut best_x = pop.row(best_idx).to_owned();
1206
1207 if self.config.disp {
1208 eprintln!(
1209 " Initial best: fitness={:.6e} at index {}",
1210 best_f, best_idx
1211 );
1212 let param_summary: Vec<String> = (0..best_x.len() / 3)
1213 .map(|i| {
1214 let freq = 10f64.powf(best_x[i * 3]);
1215 let q = best_x[i * 3 + 1];
1216 let gain = best_x[i * 3 + 2];
1217 format!("f{:.0}Hz/Q{:.2}/G{:.2}dB", freq, q, gain)
1218 })
1219 .collect();
1220 eprintln!(" Initial best params: [{}]", param_summary.join(", "));
1221 }
1222
1223 if self.config.disp {
1224 eprintln!("DE iter {:4} best_f={:.6e}", 0, best_f);
1225 }
1226
1227 let mut adaptive_state = if matches!(
1229 self.config.strategy,
1230 Strategy::AdaptiveBin | Strategy::AdaptiveExp
1231 ) || self.config.adaptive.adaptive_mutation
1232 {
1233 Some(AdaptiveState::new(&self.config.adaptive))
1234 } else {
1235 None
1236 };
1237
1238 let external_archive: Option<Arc<RwLock<external_archive::ExternalArchive>>> = if matches!(
1240 self.config.strategy,
1241 Strategy::LShadeBin | Strategy::LShadeExp
1242 ) {
1243 Some(Arc::new(RwLock::new(
1244 external_archive::ExternalArchive::with_population_size(
1245 npop,
1246 self.config.lshade.arc_rate,
1247 ),
1248 )))
1249 } else {
1250 None
1251 };
1252
1253 let mut success = false;
1255 let mut message = String::new();
1256 let mut nit = 0;
1257 let mut accepted_trials;
1258 let mut improvement_count;
1259
1260 let mut t_build_tot = std::time::Duration::ZERO;
1261 let mut t_eval_tot = std::time::Duration::ZERO;
1262 let mut t_select_tot = std::time::Duration::ZERO;
1263 let mut t_iter_tot = std::time::Duration::ZERO;
1264
1265 for iter in 1..=self.config.maxiter {
1266 nit = iter;
1267 accepted_trials = 0;
1268 improvement_count = 0;
1269
1270 let iter_start = Instant::now();
1271
1272 let sorted_indices = if matches!(
1274 self.config.strategy,
1275 Strategy::AdaptiveBin
1276 | Strategy::AdaptiveExp
1277 | Strategy::LShadeBin
1278 | Strategy::LShadeExp
1279 ) {
1280 let mut indices: Vec<usize> = (0..npop).collect();
1281 indices.sort_by(|&a, &b| {
1282 energies[a]
1283 .partial_cmp(&energies[b])
1284 .unwrap_or(std::cmp::Ordering::Equal)
1285 });
1286 indices
1287 } else {
1288 Vec::new() };
1290
1291 let t_build0 = Instant::now();
1293
1294 use rayon::prelude::*;
1296 let (trials, trial_params): (Vec<_>, Vec<_>) = (0..npop)
1297 .into_par_iter()
1298 .map(|i| {
1299 let mut local_rng: StdRng = if let Some(base_seed) = self.config.seed {
1301 StdRng::seed_from_u64(
1302 base_seed
1303 .wrapping_add((iter as u64) << 32)
1304 .wrapping_add(i as u64),
1305 )
1306 } else {
1307 let mut thread_rng = rand::rng();
1309 StdRng::from_rng(&mut thread_rng)
1310 };
1311
1312 let (f, cr) = if let Some(ref adaptive) = adaptive_state {
1314 let adaptive_f = adaptive.sample_f(&mut local_rng);
1316 let adaptive_cr = adaptive.sample_cr(&mut local_rng);
1317 (adaptive_f, adaptive_cr)
1318 } else {
1319 (
1321 self.config.mutation.sample(&mut local_rng),
1322 self.config.recombination,
1323 )
1324 };
1325
1326 let (mutant, cross) = match self.config.strategy {
1328 Strategy::Best1Bin => (
1329 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1330 Crossover::Binomial,
1331 ),
1332 Strategy::Best1Exp => (
1333 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1334 Crossover::Exponential,
1335 ),
1336 Strategy::Rand1Bin => (
1337 mutant_rand1(i, &pop, f, &mut local_rng),
1338 Crossover::Binomial,
1339 ),
1340 Strategy::Rand1Exp => (
1341 mutant_rand1(i, &pop, f, &mut local_rng),
1342 Crossover::Exponential,
1343 ),
1344 Strategy::Rand2Bin => (
1345 mutant_rand2(i, &pop, f, &mut local_rng),
1346 Crossover::Binomial,
1347 ),
1348 Strategy::Rand2Exp => (
1349 mutant_rand2(i, &pop, f, &mut local_rng),
1350 Crossover::Exponential,
1351 ),
1352 Strategy::CurrentToBest1Bin => (
1353 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1354 Crossover::Binomial,
1355 ),
1356 Strategy::CurrentToBest1Exp => (
1357 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1358 Crossover::Exponential,
1359 ),
1360 Strategy::Best2Bin => (
1361 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1362 Crossover::Binomial,
1363 ),
1364 Strategy::Best2Exp => (
1365 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1366 Crossover::Exponential,
1367 ),
1368 Strategy::RandToBest1Bin => (
1369 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1370 Crossover::Binomial,
1371 ),
1372 Strategy::RandToBest1Exp => (
1373 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1374 Crossover::Exponential,
1375 ),
1376 Strategy::AdaptiveBin => {
1377 if let Some(ref adaptive) = adaptive_state {
1378 (
1379 mutant_adaptive(
1380 i,
1381 &pop,
1382 &sorted_indices,
1383 adaptive.current_w,
1384 f,
1385 &mut local_rng,
1386 ),
1387 Crossover::Binomial,
1388 )
1389 } else {
1390 (
1392 mutant_rand1(i, &pop, f, &mut local_rng),
1393 Crossover::Binomial,
1394 )
1395 }
1396 }
1397 Strategy::AdaptiveExp => {
1398 if let Some(ref adaptive) = adaptive_state {
1399 (
1400 mutant_adaptive(
1401 i,
1402 &pop,
1403 &sorted_indices,
1404 adaptive.current_w,
1405 f,
1406 &mut local_rng,
1407 ),
1408 Crossover::Exponential,
1409 )
1410 } else {
1411 (
1413 mutant_rand1(i, &pop, f, &mut local_rng),
1414 Crossover::Exponential,
1415 )
1416 }
1417 }
1418 Strategy::LShadeBin => {
1419 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1420 self.config.lshade.p,
1421 pop.nrows(),
1422 );
1423 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1424 (
1425 mutant_current_to_pbest1::mutant_current_to_pbest1(
1426 i,
1427 &pop,
1428 &sorted_indices,
1429 pbest_size,
1430 archive_ref.as_deref(),
1431 f,
1432 &mut local_rng,
1433 ),
1434 Crossover::Binomial,
1435 )
1436 }
1437 Strategy::LShadeExp => {
1438 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1439 self.config.lshade.p,
1440 pop.nrows(),
1441 );
1442 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1443 (
1444 mutant_current_to_pbest1::mutant_current_to_pbest1(
1445 i,
1446 &pop,
1447 &sorted_indices,
1448 pbest_size,
1449 archive_ref.as_deref(),
1450 f,
1451 &mut local_rng,
1452 ),
1453 Crossover::Exponential,
1454 )
1455 }
1456 };
1457
1458 let crossover = cross;
1460 let trial = match crossover {
1461 Crossover::Binomial => {
1462 binomial_crossover(&pop.row(i).to_owned(), &mutant, cr, &mut local_rng)
1463 }
1464 Crossover::Exponential => exponential_crossover(
1465 &pop.row(i).to_owned(),
1466 &mutant,
1467 cr,
1468 &mut local_rng,
1469 ),
1470 };
1471
1472 let wls_trial = if self.config.adaptive.wls_enabled
1474 && local_rng.random::<f64>() < self.config.adaptive.wls_prob
1475 {
1476 apply_wls(
1477 &trial,
1478 &self.lower,
1479 &self.upper,
1480 self.config.adaptive.wls_scale,
1481 &mut local_rng,
1482 )
1483 } else {
1484 trial.clone()
1485 };
1486
1487 let mut trial_clipped = wls_trial;
1489 Zip::from(&mut trial_clipped)
1490 .and(&self.lower)
1491 .and(&self.upper)
1492 .for_each(|x, lo, hi| *x = x.clamp(*lo, *hi));
1493
1494 if let Some(mask) = &self.config.integrality {
1496 apply_integrality(&mut trial_clipped, mask, &self.lower, &self.upper);
1497 }
1498
1499 (trial_clipped, (f, cr))
1501 })
1502 .unzip();
1503
1504 let t_build = t_build0.elapsed();
1505 let t_eval0 = Instant::now();
1506 let trial_energies =
1507 evaluate_trials_parallel(&trials, energy_fn.clone(), &self.config.parallel);
1508 let t_eval = t_eval0.elapsed();
1509 nfev += npop;
1510
1511 let t_select0 = Instant::now();
1512 for (i, (trial, trial_energy)) in
1514 trials.into_iter().zip(trial_energies.iter()).enumerate()
1515 {
1516 let (f, cr) = trial_params[i];
1517
1518 if *trial_energy <= energies[i] {
1520 if let Some(ref archive) = external_archive
1522 && *trial_energy < energies[i]
1523 && let Ok(mut arch) = archive.write()
1524 {
1525 arch.add(pop.row(i).to_owned());
1526 }
1527 pop.row_mut(i).assign(&trial.view());
1528 energies[i] = *trial_energy;
1529 accepted_trials += 1;
1530
1531 if let Some(ref mut adaptive) = adaptive_state {
1533 adaptive.record_success(f, cr);
1534 }
1535
1536 if *trial_energy < best_f {
1538 improvement_count += 1;
1539 }
1540 }
1541 }
1542 let t_select = t_select0.elapsed();
1543
1544 t_build_tot += t_build;
1545 t_eval_tot += t_eval;
1546 t_select_tot += t_select;
1547 let iter_dur = iter_start.elapsed();
1548 t_iter_tot += iter_dur;
1549
1550 if timing_enabled && (iter <= 5 || iter % 10 == 0) {
1551 eprintln!(
1552 "TIMING iter {:4}: build={:.3} ms, eval={:.3} ms, select={:.3} ms, total={:.3} ms",
1553 iter,
1554 t_build.as_secs_f64() * 1e3,
1555 t_eval.as_secs_f64() * 1e3,
1556 t_select.as_secs_f64() * 1e3,
1557 iter_dur.as_secs_f64() * 1e3,
1558 );
1559 }
1560
1561 if let Some(ref mut adaptive) = adaptive_state {
1563 adaptive.update(&self.config.adaptive, iter, self.config.maxiter);
1564 }
1565
1566 let (new_best_idx, new_best_f) = argmin(&energies);
1568 if new_best_f < best_f {
1569 best_idx = new_best_idx;
1570 best_f = new_best_f;
1571 best_x = pop.row(best_idx).to_owned();
1572 }
1573
1574 let pop_mean = energies.mean().unwrap_or(0.0);
1576 let pop_std = energies.std(0.0);
1577 let convergence_threshold = self.config.atol + self.config.tol * pop_mean.abs();
1578
1579 if self.config.disp {
1580 eprintln!(
1581 "DE iter {:4} best_f={:.6e} std={:.3e} accepted={}/{}, improved={}",
1582 iter, best_f, pop_std, accepted_trials, npop, improvement_count
1583 );
1584 }
1585
1586 if let Some(ref mut cb) = self.config.callback {
1588 let intermediate = DEIntermediate {
1589 x: best_x.clone(),
1590 fun: best_f,
1591 convergence: pop_std,
1592 iter,
1593 };
1594 match cb(&intermediate) {
1595 CallbackAction::Stop => {
1596 success = true;
1597 message = "Optimization stopped by callback".to_string();
1598 break;
1599 }
1600 CallbackAction::Continue => {}
1601 }
1602 }
1603
1604 if pop_std <= convergence_threshold {
1605 success = true;
1606 message = format!(
1607 "Converged: std(pop_f)={:.3e} <= threshold={:.3e}",
1608 pop_std, convergence_threshold
1609 );
1610 break;
1611 }
1612 }
1613
1614 if !success {
1615 message = format!("Maximum iterations reached: {}", self.config.maxiter);
1616 }
1617
1618 if self.config.disp {
1619 eprintln!("DE finished: {}", message);
1620 }
1621
1622 let (final_x, final_f, polish_nfev) = if let Some(ref polish_cfg) = self.config.polish {
1624 if polish_cfg.enabled {
1625 self.polish(&best_x)
1626 } else {
1627 (best_x.clone(), best_f, 0)
1628 }
1629 } else {
1630 (best_x.clone(), best_f, 0)
1631 };
1632
1633 if timing_enabled {
1634 eprintln!(
1635 "TIMING total: build={:.3} s, eval={:.3} s, select={:.3} s, iter_total={:.3} s",
1636 t_build_tot.as_secs_f64(),
1637 t_eval_tot.as_secs_f64(),
1638 t_select_tot.as_secs_f64(),
1639 t_iter_tot.as_secs_f64()
1640 );
1641 }
1642
1643 self.finish_report(
1644 pop,
1645 energies,
1646 final_x,
1647 final_f,
1648 success,
1649 message,
1650 nit,
1651 nfev + polish_nfev,
1652 )
1653 }
1654}
1655
1656#[cfg(test)]
1657mod strategy_tests {
1658 use super::*;
1659
1660 #[test]
1661 fn test_parse_strategy_variants() {
1662 assert!(matches!(
1663 "best1exp".parse::<Strategy>().unwrap(),
1664 Strategy::Best1Exp
1665 ));
1666 assert!(matches!(
1667 "rand1bin".parse::<Strategy>().unwrap(),
1668 Strategy::Rand1Bin
1669 ));
1670 assert!(matches!(
1671 "randtobest1exp".parse::<Strategy>().unwrap(),
1672 Strategy::RandToBest1Exp
1673 ));
1674 }
1675}