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