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 rand::rngs::StdRng;
90use rand::{Rng, SeedableRng};
91use std::time::Instant;
92
93pub mod stack_linear_penalty;
95
96pub mod apply_integrality;
98pub mod apply_wls;
100
101pub mod distinct_indices;
103pub mod external_archive;
105pub mod init_latin_hypercube;
107pub mod init_random;
109pub mod init_sobol;
111pub mod lshade;
113
114pub mod mutant_adaptive;
116pub mod mutant_best1;
118pub mod mutant_best2;
120pub mod mutant_current_to_best1;
122pub mod mutant_current_to_pbest1;
124pub mod mutant_rand1;
126pub mod mutant_rand2;
128pub mod mutant_rand_to_best1;
130
131pub mod crossover_binomial;
133pub mod crossover_exponential;
135
136#[cfg(test)]
138mod de_tests;
139pub mod differential_evolution;
141pub mod function_registry;
143pub mod impl_helpers;
145pub mod metadata;
147pub mod parallel_eval;
149pub mod recorder;
151pub mod run_recorded;
153pub use differential_evolution::differential_evolution;
154pub use external_archive::ExternalArchive;
155pub use lshade::LShadeConfig;
156pub use parallel_eval::ParallelConfig;
157pub use recorder::{OptimizationRecord, OptimizationRecorder};
158pub use run_recorded::run_recorded_differential_evolution;
159
160pub type ScalarConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
163pub type VectorConstraintFn = Arc<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
165pub type PenaltyTuple = (ScalarConstraintFn, f64);
167pub type CallbackFn = Box<dyn FnMut(&DEIntermediate) -> CallbackAction>;
169
170pub(crate) fn argmin(v: &Array1<f64>) -> (usize, f64) {
171 let mut best_i = 0usize;
172 let mut best_v = v[0];
173 for (i, &val) in v.iter().enumerate() {
174 if val < best_v {
175 best_v = val;
176 best_i = i;
177 }
178 }
179 (best_i, best_v)
180}
181
182#[derive(Debug, Clone, Copy)]
189pub enum Strategy {
190 Best1Bin,
192 Best1Exp,
194 Rand1Bin,
196 Rand1Exp,
198 Rand2Bin,
200 Rand2Exp,
202 CurrentToBest1Bin,
204 CurrentToBest1Exp,
206 Best2Bin,
208 Best2Exp,
210 RandToBest1Bin,
212 RandToBest1Exp,
214 AdaptiveBin,
216 AdaptiveExp,
218 LShadeBin,
220 LShadeExp,
222}
223
224impl FromStr for Strategy {
225 type Err = String;
226 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
227 let t = s.to_lowercase();
228 match t.as_str() {
229 "best1bin" | "best1" => Ok(Strategy::Best1Bin),
230 "best1exp" => Ok(Strategy::Best1Exp),
231 "rand1bin" | "rand1" => Ok(Strategy::Rand1Bin),
232 "rand1exp" => Ok(Strategy::Rand1Exp),
233 "rand2bin" | "rand2" => Ok(Strategy::Rand2Bin),
234 "rand2exp" => Ok(Strategy::Rand2Exp),
235 "currenttobest1bin" | "current-to-best1bin" | "current_to_best1bin" => {
236 Ok(Strategy::CurrentToBest1Bin)
237 }
238 "currenttobest1exp" | "current-to-best1exp" | "current_to_best1exp" => {
239 Ok(Strategy::CurrentToBest1Exp)
240 }
241 "best2bin" | "best2" => Ok(Strategy::Best2Bin),
242 "best2exp" => Ok(Strategy::Best2Exp),
243 "randtobest1bin" | "rand-to-best1bin" | "rand_to_best1bin" => {
244 Ok(Strategy::RandToBest1Bin)
245 }
246 "randtobest1exp" | "rand-to-best1exp" | "rand_to_best1exp" => {
247 Ok(Strategy::RandToBest1Exp)
248 }
249 "adaptivebin" | "adaptive-bin" | "adaptive_bin" | "adaptive" => {
250 Ok(Strategy::AdaptiveBin)
251 }
252 "adaptiveexp" | "adaptive-exp" | "adaptive_exp" => Ok(Strategy::AdaptiveExp),
253 "lshadebin" | "lshade-bin" | "lshade_bin" | "lshade" => Ok(Strategy::LShadeBin),
254 "lshadeexp" | "lshade-exp" | "lshade_exp" => Ok(Strategy::LShadeExp),
255 _ => Err(format!("unknown strategy: {}", s)),
256 }
257 }
258}
259
260#[derive(Debug, Clone, Copy, Default)]
262pub enum Crossover {
263 #[default]
265 Binomial,
266 Exponential,
268}
269
270#[derive(Debug, Clone, Copy)]
272pub enum Mutation {
273 Factor(f64),
275 Range {
277 min: f64,
279 max: f64,
281 },
282 Adaptive {
284 initial_f: f64,
286 },
287}
288
289impl Default for Mutation {
290 fn default() -> Self {
291 let _ = Mutation::Factor(0.8);
292 Mutation::Range { min: 0.0, max: 2.0 }
293 }
294}
295
296impl Mutation {
297 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
298 match *self {
299 Mutation::Factor(f) => f,
300 Mutation::Range { min, max } => rng.random_range(min..max),
301 Mutation::Adaptive { initial_f } => initial_f, }
303 }
304
305 #[allow(dead_code)]
307 fn sample_cauchy<R: Rng + ?Sized>(&self, f_m: f64, _scale: f64, rng: &mut R) -> f64 {
308 let perturbation = (rng.random::<f64>() - 0.5) * 0.2; (f_m + perturbation).clamp(0.0, 2.0) }
312}
313
314#[derive(Debug, Clone, Copy, Default)]
316pub enum Init {
317 #[default]
319 LatinHypercube,
320 Random,
322}
323
324#[derive(Debug, Clone, Copy, Default)]
326pub enum Updating {
327 #[default]
329 Deferred,
330}
331
332#[derive(Debug, Clone)]
334pub struct LinearPenalty {
335 pub a: Array2<f64>,
337 pub lb: Array1<f64>,
339 pub ub: Array1<f64>,
341 pub weight: f64,
343}
344
345#[derive(Debug, Clone)]
347pub struct LinearConstraintHelper {
348 pub a: Array2<f64>,
350 pub lb: Array1<f64>,
352 pub ub: Array1<f64>,
354}
355
356impl LinearConstraintHelper {
357 pub fn apply_to(&self, cfg: &mut DEConfig, weight: f64) {
359 use stack_linear_penalty::stack_linear_penalty;
360
361 let new_lp = LinearPenalty {
362 a: self.a.clone(),
363 lb: self.lb.clone(),
364 ub: self.ub.clone(),
365 weight,
366 };
367 match &mut cfg.linear_penalty {
368 Some(existing) => stack_linear_penalty(existing, &new_lp),
369 None => cfg.linear_penalty = Some(new_lp),
370 }
371 }
372}
373
374#[derive(Clone)]
376pub struct NonlinearConstraintHelper {
377 pub fun: VectorConstraintFn,
379 pub lb: Array1<f64>,
381 pub ub: Array1<f64>,
383}
384
385impl NonlinearConstraintHelper {
386 pub fn apply_to(&self, cfg: &mut DEConfig, weight_ineq: f64, weight_eq: f64) {
390 let f = self.fun.clone();
391 let lb = self.lb.clone();
392 let ub = self.ub.clone();
393 let m = lb.len().min(ub.len());
394 for i in 0..m {
395 let l = lb[i];
396 let u = ub[i];
397 let tol = 1e-12 * (l.abs() + u.abs()).max(1.0);
398 if (u - l).abs() <= tol {
399 let fi = f.clone();
400 cfg.penalty_eq.push((
401 Arc::new(move |x: &Array1<f64>| {
402 let y = (fi)(x);
403 y[i] - l
404 }),
405 weight_eq,
406 ));
407 } else {
408 let fi_u = f.clone();
409 cfg.penalty_ineq.push((
410 Arc::new(move |x: &Array1<f64>| {
411 let y = (fi_u)(x);
412 y[i] - u
413 }),
414 weight_ineq,
415 ));
416 let fi_l = f.clone();
417 cfg.penalty_ineq.push((
418 Arc::new(move |x: &Array1<f64>| {
419 let y = (fi_l)(x);
420 l - y[i]
421 }),
422 weight_ineq,
423 ));
424 }
425 }
426 }
427}
428
429#[derive(Debug, Clone)]
431struct AdaptiveState {
432 f_m: f64,
434 cr_m: f64,
436 successful_f: Vec<f64>,
438 successful_cr: Vec<f64>,
440 current_w: f64,
442}
443
444impl AdaptiveState {
445 fn new(config: &AdaptiveConfig) -> Self {
446 Self {
447 f_m: config.f_m,
448 cr_m: config.cr_m,
449 successful_f: Vec::new(),
450 successful_cr: Vec::new(),
451 current_w: config.w_max, }
453 }
454
455 fn update(&mut self, config: &AdaptiveConfig, iter: usize, max_iter: usize) {
457 let iter_ratio = iter as f64 / max_iter as f64;
459 self.current_w = config.w_max - (config.w_max - config.w_min) * iter_ratio;
460
461 if !self.successful_f.is_empty() {
463 let mean_f = self.compute_lehmer_mean(&self.successful_f);
464 self.f_m = (1.0 - config.w_f) * self.f_m + config.w_f * mean_f;
465 self.f_m = self.f_m.clamp(0.2, 1.2);
466 }
467
468 if !self.successful_cr.is_empty() {
470 let mean_cr = self.compute_arithmetic_mean(&self.successful_cr);
471 self.cr_m = (1.0 - config.w_cr) * self.cr_m + config.w_cr * mean_cr;
472 self.cr_m = self.cr_m.clamp(0.1, 0.9);
473 }
474
475 self.successful_f.clear();
477 self.successful_cr.clear();
478 }
479
480 fn compute_lehmer_mean(&self, values: &[f64]) -> f64 {
482 if values.is_empty() {
483 return 0.5; }
485
486 let sum_sq: f64 = values.iter().map(|&x| x * x).sum();
487 let sum: f64 = values.iter().sum();
488
489 if sum > 0.0 {
490 sum_sq / sum
491 } else {
492 0.5 }
494 }
495
496 fn compute_arithmetic_mean(&self, values: &[f64]) -> f64 {
498 if values.is_empty() {
499 return 0.5; }
501 values.iter().sum::<f64>() / values.len() as f64
502 }
503
504 fn record_success(&mut self, f_val: f64, cr_val: f64) {
506 self.successful_f.push(f_val);
507 self.successful_cr.push(cr_val);
508 }
509
510 fn sample_f<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
512 let u1: f64 = rng.random::<f64>().max(1e-15);
513 let u2: f64 = rng.random::<f64>();
514
515 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
516 let sample = self.f_m + 0.05 * normal;
517
518 sample.clamp(0.3, 1.0)
519 }
520
521 fn sample_cr<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
523 let u1: f64 = rng.random::<f64>().max(1e-15);
524 let u2: f64 = rng.random::<f64>();
525
526 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
527 let sample = self.cr_m + 0.05 * normal;
528
529 sample.clamp(0.1, 0.9)
530 }
531}
532
533#[derive(Debug, Clone)]
535pub struct AdaptiveConfig {
536 pub adaptive_mutation: bool,
538 pub wls_enabled: bool,
540 pub w_max: f64,
542 pub w_min: f64,
544 pub w_f: f64,
546 pub w_cr: f64,
548 pub f_m: f64,
550 pub cr_m: f64,
552 pub wls_prob: f64,
554 pub wls_scale: f64,
556}
557
558impl Default for AdaptiveConfig {
559 fn default() -> Self {
560 Self {
561 adaptive_mutation: false,
562 wls_enabled: false,
563 w_max: 0.9,
564 w_min: 0.1,
565 w_f: 0.9,
566 w_cr: 0.9,
567 f_m: 0.5,
568 cr_m: 0.6,
569 wls_prob: 0.1,
570 wls_scale: 0.1,
571 }
572 }
573}
574
575#[derive(Debug, Clone)]
577pub struct PolishConfig {
578 pub enabled: bool,
580 pub algo: String,
582 pub maxeval: usize,
584}
585
586pub struct DEConfig {
592 pub maxiter: usize,
594 pub popsize: usize,
596 pub tol: f64,
598 pub atol: f64,
600 pub mutation: Mutation,
602 pub recombination: f64,
604 pub strategy: Strategy,
606 pub crossover: Crossover,
608 pub init: Init,
610 pub updating: Updating,
612 pub seed: Option<u64>,
614 pub integrality: Option<Vec<bool>>,
616 pub x0: Option<Array1<f64>>,
618 pub disp: bool,
620 pub callback: Option<CallbackFn>,
622 pub penalty_ineq: Vec<PenaltyTuple>,
624 pub penalty_eq: Vec<PenaltyTuple>,
626 pub linear_penalty: Option<LinearPenalty>,
628 pub polish: Option<PolishConfig>,
630 pub adaptive: AdaptiveConfig,
632 pub parallel: parallel_eval::ParallelConfig,
634 pub lshade: lshade::LShadeConfig,
636}
637
638impl Default for DEConfig {
639 fn default() -> Self {
640 Self {
641 maxiter: 1000,
642 popsize: 15,
643 tol: 1e-2,
644 atol: 0.0,
645 mutation: Mutation::default(),
646 recombination: 0.7,
647 strategy: Strategy::Best1Bin,
648 crossover: Crossover::default(),
649 init: Init::default(),
650 updating: Updating::default(),
651 seed: None,
652 integrality: None,
653 x0: None,
654 disp: false,
655 callback: None,
656 penalty_ineq: Vec::new(),
657 penalty_eq: Vec::new(),
658 linear_penalty: None,
659 polish: None,
660 adaptive: AdaptiveConfig::default(),
661 parallel: parallel_eval::ParallelConfig::default(),
662 lshade: lshade::LShadeConfig::default(),
663 }
664 }
665}
666
667pub struct DEConfigBuilder {
684 cfg: DEConfig,
685}
686impl Default for DEConfigBuilder {
687 fn default() -> Self {
688 Self::new()
689 }
690}
691
692impl DEConfigBuilder {
693 pub fn new() -> Self {
695 Self {
696 cfg: DEConfig::default(),
697 }
698 }
699 pub fn maxiter(mut self, v: usize) -> Self {
701 self.cfg.maxiter = v;
702 self
703 }
704 pub fn popsize(mut self, v: usize) -> Self {
711 self.cfg.popsize = v;
712 self
713 }
714 pub fn tol(mut self, v: f64) -> Self {
716 self.cfg.tol = v;
717 self
718 }
719 pub fn atol(mut self, v: f64) -> Self {
721 self.cfg.atol = v;
722 self
723 }
724 pub fn mutation(mut self, v: Mutation) -> Self {
726 self.cfg.mutation = v;
727 self
728 }
729 pub fn recombination(mut self, v: f64) -> Self {
731 self.cfg.recombination = v;
732 self
733 }
734 pub fn strategy(mut self, v: Strategy) -> Self {
736 self.cfg.strategy = v;
737 self
738 }
739 pub fn crossover(mut self, v: Crossover) -> Self {
741 self.cfg.crossover = v;
742 self
743 }
744 pub fn init(mut self, v: Init) -> Self {
746 self.cfg.init = v;
747 self
748 }
749 pub fn seed(mut self, v: u64) -> Self {
751 self.cfg.seed = Some(v);
752 self
753 }
754 pub fn integrality(mut self, v: Vec<bool>) -> Self {
756 self.cfg.integrality = Some(v);
757 self
758 }
759 pub fn x0(mut self, v: Array1<f64>) -> Self {
761 self.cfg.x0 = Some(v);
762 self
763 }
764 pub fn disp(mut self, v: bool) -> Self {
766 self.cfg.disp = v;
767 self
768 }
769 pub fn callback(mut self, cb: Box<dyn FnMut(&DEIntermediate) -> CallbackAction>) -> Self {
771 self.cfg.callback = Some(cb);
772 self
773 }
774 pub fn add_penalty_ineq<FN>(mut self, f: FN, w: f64) -> Self
776 where
777 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
778 {
779 self.cfg.penalty_ineq.push((Arc::new(f), w));
780 self
781 }
782 pub fn add_penalty_eq<FN>(mut self, f: FN, w: f64) -> Self
784 where
785 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
786 {
787 self.cfg.penalty_eq.push((Arc::new(f), w));
788 self
789 }
790 pub fn linear_penalty(mut self, lp: LinearPenalty) -> Self {
792 self.cfg.linear_penalty = Some(lp);
793 self
794 }
795 pub fn polish(mut self, pol: PolishConfig) -> Self {
797 self.cfg.polish = Some(pol);
798 self
799 }
800 pub fn adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
802 self.cfg.adaptive = adaptive;
803 self
804 }
805 pub fn enable_adaptive_mutation(mut self, enable: bool) -> Self {
807 self.cfg.adaptive.adaptive_mutation = enable;
808 self
809 }
810 pub fn enable_wls(mut self, enable: bool) -> Self {
812 self.cfg.adaptive.wls_enabled = enable;
813 self
814 }
815 pub fn adaptive_weights(mut self, w_max: f64, w_min: f64) -> Self {
817 self.cfg.adaptive.w_max = w_max;
818 self.cfg.adaptive.w_min = w_min;
819 self
820 }
821 pub fn parallel(mut self, parallel: parallel_eval::ParallelConfig) -> Self {
823 self.cfg.parallel = parallel;
824 self
825 }
826 pub fn enable_parallel(mut self, enable: bool) -> Self {
828 self.cfg.parallel.enabled = enable;
829 self
830 }
831 pub fn parallel_threads(mut self, num_threads: usize) -> Self {
833 self.cfg.parallel.num_threads = Some(num_threads);
834 self
835 }
836 pub fn lshade(mut self, lshade: lshade::LShadeConfig) -> Self {
838 self.cfg.lshade = lshade;
839 self
840 }
841 pub fn build(self) -> error::Result<DEConfig> {
847 if self.cfg.popsize < 4 {
848 return Err(DEError::PopulationTooSmall {
849 pop_size: self.cfg.popsize,
850 });
851 }
852 Ok(self.cfg)
853 }
854}
855
856#[derive(Clone)]
860pub struct DEReport {
861 pub x: Array1<f64>,
863 pub fun: f64,
865 pub success: bool,
867 pub message: String,
869 pub nit: usize,
871 pub nfev: usize,
873 pub population: Array2<f64>,
875 pub population_energies: Array1<f64>,
877}
878
879impl fmt::Debug for DEReport {
880 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
881 f.debug_struct("DEReport")
882 .field("x", &format!("len={}", self.x.len()))
883 .field("fun", &self.fun)
884 .field("success", &self.success)
885 .field("message", &self.message)
886 .field("nit", &self.nit)
887 .field("nfev", &self.nfev)
888 .field(
889 "population",
890 &format!("{}x{}", self.population.nrows(), self.population.ncols()),
891 )
892 .field(
893 "population_energies",
894 &format!("len={}", self.population_energies.len()),
895 )
896 .finish()
897 }
898}
899
900pub struct DEIntermediate {
902 pub x: Array1<f64>,
904 pub fun: f64,
906 pub convergence: f64,
908 pub iter: usize,
910}
911
912pub enum CallbackAction {
914 Continue,
916 Stop,
918}
919
920pub struct DifferentialEvolution<'a, F>
926where
927 F: Fn(&Array1<f64>) -> f64 + Sync,
928{
929 func: &'a F,
930 lower: Array1<f64>,
931 upper: Array1<f64>,
932 config: DEConfig,
933}
934
935impl<'a, F> DifferentialEvolution<'a, F>
936where
937 F: Fn(&Array1<f64>) -> f64 + Sync,
938{
939 pub fn new(func: &'a F, lower: Array1<f64>, upper: Array1<f64>) -> Result<Self> {
946 if lower.len() != upper.len() {
947 return Err(DEError::BoundsMismatch {
948 lower_len: lower.len(),
949 upper_len: upper.len(),
950 });
951 }
952
953 for i in 0..lower.len() {
955 if lower[i] > upper[i] {
956 return Err(DEError::InvalidBounds {
957 index: i,
958 lower: lower[i],
959 upper: upper[i],
960 });
961 }
962 }
963
964 Ok(Self {
965 func,
966 lower,
967 upper,
968 config: DEConfig::default(),
969 })
970 }
971
972 pub fn config_mut(&mut self) -> &mut DEConfig {
974 &mut self.config
975 }
976
977 pub fn solve(&mut self) -> DEReport {
979 use apply_integrality::apply_integrality;
980 use apply_wls::apply_wls;
981 use crossover_binomial::binomial_crossover;
982 use crossover_exponential::exponential_crossover;
983 use init_latin_hypercube::init_latin_hypercube;
984 use init_random::init_random;
985 use mutant_adaptive::mutant_adaptive;
986 use mutant_best1::mutant_best1;
987 use mutant_best2::mutant_best2;
988 use mutant_current_to_best1::mutant_current_to_best1;
989 use mutant_current_to_pbest1;
990 use mutant_rand_to_best1::mutant_rand_to_best1;
991 use mutant_rand1::mutant_rand1;
992 use mutant_rand2::mutant_rand2;
993 use parallel_eval::evaluate_trials_parallel;
994 use std::sync::Arc;
995
996 let n = self.lower.len();
997
998 let mut is_free: Vec<bool> = Vec::with_capacity(n);
1000 for i in 0..n {
1001 is_free.push((self.upper[i] - self.lower[i]).abs() > 0.0);
1002 }
1003 let n_free = is_free.iter().filter(|&&b| b).count();
1004 let _n_equal = n - n_free;
1005 if n_free == 0 {
1006 let x_fixed = self.lower.clone();
1008 let mut x_eval = x_fixed.clone();
1009 if let Some(mask) = &self.config.integrality {
1010 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1011 }
1012 let f = (self.func)(&x_eval);
1013 return DEReport {
1014 x: x_eval,
1015 fun: f,
1016 success: true,
1017 message: "All variables fixed by bounds".into(),
1018 nit: 0,
1019 nfev: 1,
1020 population: Array2::zeros((1, n)),
1021 population_energies: Array1::from(vec![f]),
1022 };
1023 }
1024
1025 let npop = self.config.popsize * n_free;
1026 let _bounds_span = &self.upper - &self.lower;
1027
1028 if self.config.disp {
1029 eprintln!(
1030 "DE Init: {} dimensions ({} free), population={}, maxiter={}",
1031 n, n_free, npop, self.config.maxiter
1032 );
1033 eprintln!(
1034 " Strategy: {:?}, Mutation: {:?}, Crossover: CR={:.3}",
1035 self.config.strategy, self.config.mutation, self.config.recombination
1036 );
1037 eprintln!(
1038 " Tolerances: tol={:.2e}, atol={:.2e}",
1039 self.config.tol, self.config.atol
1040 );
1041 }
1042
1043 let timing_enabled = std::env::var("AUTOEQ_DE_TIMING")
1045 .map(|v| v != "0")
1046 .unwrap_or(false);
1047
1048 if let Some(n) = self.config.parallel.num_threads {
1050 let _ = rayon::ThreadPoolBuilder::new()
1052 .num_threads(n)
1053 .build_global();
1054 }
1055
1056 let mut rng: StdRng = match self.config.seed {
1058 Some(s) => StdRng::seed_from_u64(s),
1059 None => {
1060 let mut thread_rng = rand::rng();
1061 StdRng::from_rng(&mut thread_rng)
1062 }
1063 };
1064
1065 let mut pop = match self.config.init {
1067 Init::LatinHypercube => {
1068 if self.config.disp {
1069 eprintln!(" Using Latin Hypercube initialization");
1070 }
1071 init_latin_hypercube(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1072 }
1073 Init::Random => {
1074 if self.config.disp {
1075 eprintln!(" Using Random initialization");
1076 }
1077 init_random(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1078 }
1079 };
1080
1081 let mut nfev: usize = 0;
1083 if self.config.disp {
1084 eprintln!(" Evaluating initial population of {} individuals...", npop);
1085 }
1086
1087 let mut eval_pop = pop.clone();
1089 let t_integrality0 = Instant::now();
1090 if let Some(mask) = &self.config.integrality {
1091 for i in 0..npop {
1092 let mut row = eval_pop.row_mut(i);
1093 let mut x_eval = row.to_owned();
1094 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1095 row.assign(&x_eval);
1096 }
1097 }
1098 let t_integrality = t_integrality0.elapsed();
1099
1100 let func_ref = self.func;
1102 let penalty_ineq_vec: Vec<PenaltyTuple> = self
1103 .config
1104 .penalty_ineq
1105 .iter()
1106 .map(|(f, w)| (f.clone(), *w))
1107 .collect();
1108 let penalty_eq_vec: Vec<PenaltyTuple> = self
1109 .config
1110 .penalty_eq
1111 .iter()
1112 .map(|(f, w)| (f.clone(), *w))
1113 .collect();
1114 let linear_penalty = self.config.linear_penalty.clone();
1115
1116 let energy_fn = Arc::new(move |x: &Array1<f64>| -> f64 {
1117 let base = (func_ref)(x);
1118 let mut p = 0.0;
1119 for (f, w) in &penalty_ineq_vec {
1120 let v = f(x);
1121 let viol = v.max(0.0);
1122 p += w * viol * viol;
1123 }
1124 for (h, w) in &penalty_eq_vec {
1125 let v = h(x);
1126 p += w * v * v;
1127 }
1128 if let Some(ref lp) = linear_penalty {
1129 let ax = lp.a.dot(&x.view());
1130 Zip::from(&ax)
1131 .and(&lp.lb)
1132 .and(&lp.ub)
1133 .for_each(|&v, &lo, &hi| {
1134 if v < lo {
1135 let d = lo - v;
1136 p += lp.weight * d * d;
1137 } else if v > hi {
1138 let d = v - hi;
1139 p += lp.weight * d * d;
1140 }
1141 });
1142 }
1143 base + p
1144 });
1145
1146 let t_eval0 = Instant::now();
1147 let mut energies = parallel_eval::evaluate_population_parallel(
1148 &eval_pop,
1149 energy_fn.clone(),
1150 &self.config.parallel,
1151 );
1152 let t_eval_init = t_eval0.elapsed();
1153 nfev += npop;
1154 if timing_enabled {
1155 eprintln!(
1156 "TIMING init: integrality={:.3} ms, eval={:.3} ms",
1157 t_integrality.as_secs_f64() * 1e3,
1158 t_eval_init.as_secs_f64() * 1e3
1159 );
1160 }
1161
1162 let pop_mean = energies.mean().unwrap_or(0.0);
1164 let pop_std = energies.std(0.0);
1165 if self.config.disp {
1166 eprintln!(
1167 " Initial population: mean={:.6e}, std={:.6e}",
1168 pop_mean, pop_std
1169 );
1170 }
1171
1172 if let Some(x0) = &self.config.x0 {
1174 let mut x0c = x0.clone();
1175 for i in 0..x0c.len() {
1177 x0c[i] = x0c[i].clamp(self.lower[i], self.upper[i]);
1178 }
1179 if let Some(mask) = &self.config.integrality {
1180 apply_integrality(&mut x0c, mask, &self.lower, &self.upper);
1181 }
1182 let f0 = self.energy(&x0c);
1183 nfev += 1;
1184 let (best_idx, _best_f) = argmin(&energies);
1186 pop.row_mut(best_idx).assign(&x0c.view());
1187 energies[best_idx] = f0;
1188 }
1189
1190 let (mut best_idx, mut best_f) = argmin(&energies);
1191 let mut best_x = pop.row(best_idx).to_owned();
1192
1193 if self.config.disp {
1194 eprintln!(
1195 " Initial best: fitness={:.6e} at index {}",
1196 best_f, best_idx
1197 );
1198 let param_summary: Vec<String> = (0..best_x.len() / 3)
1199 .map(|i| {
1200 let freq = 10f64.powf(best_x[i * 3]);
1201 let q = best_x[i * 3 + 1];
1202 let gain = best_x[i * 3 + 2];
1203 format!("f{:.0}Hz/Q{:.2}/G{:.2}dB", freq, q, gain)
1204 })
1205 .collect();
1206 eprintln!(" Initial best params: [{}]", param_summary.join(", "));
1207 }
1208
1209 if self.config.disp {
1210 eprintln!("DE iter {:4} best_f={:.6e}", 0, best_f);
1211 }
1212
1213 let mut adaptive_state = if matches!(
1215 self.config.strategy,
1216 Strategy::AdaptiveBin | Strategy::AdaptiveExp
1217 ) || self.config.adaptive.adaptive_mutation
1218 {
1219 Some(AdaptiveState::new(&self.config.adaptive))
1220 } else {
1221 None
1222 };
1223
1224 let external_archive: Option<Arc<RwLock<external_archive::ExternalArchive>>> = if matches!(
1226 self.config.strategy,
1227 Strategy::LShadeBin | Strategy::LShadeExp
1228 ) {
1229 Some(Arc::new(RwLock::new(
1230 external_archive::ExternalArchive::with_population_size(
1231 npop,
1232 self.config.lshade.arc_rate,
1233 ),
1234 )))
1235 } else {
1236 None
1237 };
1238
1239 let mut success = false;
1241 let mut message = String::new();
1242 let mut nit = 0;
1243 let mut accepted_trials;
1244 let mut improvement_count;
1245
1246 let mut t_build_tot = std::time::Duration::ZERO;
1247 let mut t_eval_tot = std::time::Duration::ZERO;
1248 let mut t_select_tot = std::time::Duration::ZERO;
1249 let mut t_iter_tot = std::time::Duration::ZERO;
1250
1251 for iter in 1..=self.config.maxiter {
1252 nit = iter;
1253 accepted_trials = 0;
1254 improvement_count = 0;
1255
1256 let iter_start = Instant::now();
1257
1258 let sorted_indices = if matches!(
1260 self.config.strategy,
1261 Strategy::AdaptiveBin
1262 | Strategy::AdaptiveExp
1263 | Strategy::LShadeBin
1264 | Strategy::LShadeExp
1265 ) {
1266 let mut indices: Vec<usize> = (0..npop).collect();
1267 indices.sort_by(|&a, &b| {
1268 energies[a]
1269 .partial_cmp(&energies[b])
1270 .unwrap_or(std::cmp::Ordering::Equal)
1271 });
1272 indices
1273 } else {
1274 Vec::new() };
1276
1277 let t_build0 = Instant::now();
1279
1280 use rayon::prelude::*;
1282 let (trials, trial_params): (Vec<_>, Vec<_>) = (0..npop)
1283 .into_par_iter()
1284 .map(|i| {
1285 let mut local_rng: StdRng = if let Some(base_seed) = self.config.seed {
1287 StdRng::seed_from_u64(
1288 base_seed
1289 .wrapping_add((iter as u64) << 32)
1290 .wrapping_add(i as u64),
1291 )
1292 } else {
1293 let mut thread_rng = rand::rng();
1295 StdRng::from_rng(&mut thread_rng)
1296 };
1297
1298 let (f, cr) = if let Some(ref adaptive) = adaptive_state {
1300 let adaptive_f = adaptive.sample_f(&mut local_rng);
1302 let adaptive_cr = adaptive.sample_cr(&mut local_rng);
1303 (adaptive_f, adaptive_cr)
1304 } else {
1305 (
1307 self.config.mutation.sample(&mut local_rng),
1308 self.config.recombination,
1309 )
1310 };
1311
1312 let (mutant, cross) = match self.config.strategy {
1314 Strategy::Best1Bin => (
1315 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1316 Crossover::Binomial,
1317 ),
1318 Strategy::Best1Exp => (
1319 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1320 Crossover::Exponential,
1321 ),
1322 Strategy::Rand1Bin => (
1323 mutant_rand1(i, &pop, f, &mut local_rng),
1324 Crossover::Binomial,
1325 ),
1326 Strategy::Rand1Exp => (
1327 mutant_rand1(i, &pop, f, &mut local_rng),
1328 Crossover::Exponential,
1329 ),
1330 Strategy::Rand2Bin => (
1331 mutant_rand2(i, &pop, f, &mut local_rng),
1332 Crossover::Binomial,
1333 ),
1334 Strategy::Rand2Exp => (
1335 mutant_rand2(i, &pop, f, &mut local_rng),
1336 Crossover::Exponential,
1337 ),
1338 Strategy::CurrentToBest1Bin => (
1339 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1340 Crossover::Binomial,
1341 ),
1342 Strategy::CurrentToBest1Exp => (
1343 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1344 Crossover::Exponential,
1345 ),
1346 Strategy::Best2Bin => (
1347 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1348 Crossover::Binomial,
1349 ),
1350 Strategy::Best2Exp => (
1351 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1352 Crossover::Exponential,
1353 ),
1354 Strategy::RandToBest1Bin => (
1355 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1356 Crossover::Binomial,
1357 ),
1358 Strategy::RandToBest1Exp => (
1359 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1360 Crossover::Exponential,
1361 ),
1362 Strategy::AdaptiveBin => {
1363 if let Some(ref adaptive) = adaptive_state {
1364 (
1365 mutant_adaptive(
1366 i,
1367 &pop,
1368 &sorted_indices,
1369 adaptive.current_w,
1370 f,
1371 &mut local_rng,
1372 ),
1373 Crossover::Binomial,
1374 )
1375 } else {
1376 (
1378 mutant_rand1(i, &pop, f, &mut local_rng),
1379 Crossover::Binomial,
1380 )
1381 }
1382 }
1383 Strategy::AdaptiveExp => {
1384 if let Some(ref adaptive) = adaptive_state {
1385 (
1386 mutant_adaptive(
1387 i,
1388 &pop,
1389 &sorted_indices,
1390 adaptive.current_w,
1391 f,
1392 &mut local_rng,
1393 ),
1394 Crossover::Exponential,
1395 )
1396 } else {
1397 (
1399 mutant_rand1(i, &pop, f, &mut local_rng),
1400 Crossover::Exponential,
1401 )
1402 }
1403 }
1404 Strategy::LShadeBin => {
1405 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1406 self.config.lshade.p,
1407 pop.nrows(),
1408 );
1409 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1410 (
1411 mutant_current_to_pbest1::mutant_current_to_pbest1(
1412 i,
1413 &pop,
1414 &sorted_indices,
1415 pbest_size,
1416 archive_ref.as_deref(),
1417 f,
1418 &mut local_rng,
1419 ),
1420 Crossover::Binomial,
1421 )
1422 }
1423 Strategy::LShadeExp => {
1424 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1425 self.config.lshade.p,
1426 pop.nrows(),
1427 );
1428 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1429 (
1430 mutant_current_to_pbest1::mutant_current_to_pbest1(
1431 i,
1432 &pop,
1433 &sorted_indices,
1434 pbest_size,
1435 archive_ref.as_deref(),
1436 f,
1437 &mut local_rng,
1438 ),
1439 Crossover::Exponential,
1440 )
1441 }
1442 };
1443
1444 let crossover = cross;
1446 let trial = match crossover {
1447 Crossover::Binomial => {
1448 binomial_crossover(&pop.row(i).to_owned(), &mutant, cr, &mut local_rng)
1449 }
1450 Crossover::Exponential => exponential_crossover(
1451 &pop.row(i).to_owned(),
1452 &mutant,
1453 cr,
1454 &mut local_rng,
1455 ),
1456 };
1457
1458 let wls_trial = if self.config.adaptive.wls_enabled
1460 && local_rng.random::<f64>() < self.config.adaptive.wls_prob
1461 {
1462 apply_wls(
1463 &trial,
1464 &self.lower,
1465 &self.upper,
1466 self.config.adaptive.wls_scale,
1467 &mut local_rng,
1468 )
1469 } else {
1470 trial.clone()
1471 };
1472
1473 let mut trial_clipped = wls_trial;
1475 Zip::from(&mut trial_clipped)
1476 .and(&self.lower)
1477 .and(&self.upper)
1478 .for_each(|x, lo, hi| *x = x.clamp(*lo, *hi));
1479
1480 if let Some(mask) = &self.config.integrality {
1482 apply_integrality(&mut trial_clipped, mask, &self.lower, &self.upper);
1483 }
1484
1485 (trial_clipped, (f, cr))
1487 })
1488 .unzip();
1489
1490 let t_build = t_build0.elapsed();
1491 let t_eval0 = Instant::now();
1492 let trial_energies =
1493 evaluate_trials_parallel(&trials, energy_fn.clone(), &self.config.parallel);
1494 let t_eval = t_eval0.elapsed();
1495 nfev += npop;
1496
1497 let t_select0 = Instant::now();
1498 for (i, (trial, trial_energy)) in
1500 trials.into_iter().zip(trial_energies.iter()).enumerate()
1501 {
1502 let (f, cr) = trial_params[i];
1503
1504 if *trial_energy <= energies[i] {
1506 if let Some(ref archive) = external_archive
1508 && *trial_energy < energies[i]
1509 && let Ok(mut arch) = archive.write()
1510 {
1511 arch.add(pop.row(i).to_owned());
1512 }
1513 pop.row_mut(i).assign(&trial.view());
1514 energies[i] = *trial_energy;
1515 accepted_trials += 1;
1516
1517 if let Some(ref mut adaptive) = adaptive_state {
1519 adaptive.record_success(f, cr);
1520 }
1521
1522 if *trial_energy < best_f {
1524 improvement_count += 1;
1525 }
1526 }
1527 }
1528 let t_select = t_select0.elapsed();
1529
1530 t_build_tot += t_build;
1531 t_eval_tot += t_eval;
1532 t_select_tot += t_select;
1533 let iter_dur = iter_start.elapsed();
1534 t_iter_tot += iter_dur;
1535
1536 if timing_enabled && (iter <= 5 || iter % 10 == 0) {
1537 eprintln!(
1538 "TIMING iter {:4}: build={:.3} ms, eval={:.3} ms, select={:.3} ms, total={:.3} ms",
1539 iter,
1540 t_build.as_secs_f64() * 1e3,
1541 t_eval.as_secs_f64() * 1e3,
1542 t_select.as_secs_f64() * 1e3,
1543 iter_dur.as_secs_f64() * 1e3,
1544 );
1545 }
1546
1547 if let Some(ref mut adaptive) = adaptive_state {
1549 adaptive.update(&self.config.adaptive, iter, self.config.maxiter);
1550 }
1551
1552 let (new_best_idx, new_best_f) = argmin(&energies);
1554 if new_best_f < best_f {
1555 best_idx = new_best_idx;
1556 best_f = new_best_f;
1557 best_x = pop.row(best_idx).to_owned();
1558 }
1559
1560 let pop_mean = energies.mean().unwrap_or(0.0);
1562 let pop_std = energies.std(0.0);
1563 let convergence_threshold = self.config.atol + self.config.tol * pop_mean.abs();
1564
1565 if self.config.disp {
1566 eprintln!(
1567 "DE iter {:4} best_f={:.6e} std={:.3e} accepted={}/{}, improved={}",
1568 iter, best_f, pop_std, accepted_trials, npop, improvement_count
1569 );
1570 }
1571
1572 if let Some(ref mut cb) = self.config.callback {
1574 let intermediate = DEIntermediate {
1575 x: best_x.clone(),
1576 fun: best_f,
1577 convergence: pop_std,
1578 iter,
1579 };
1580 match cb(&intermediate) {
1581 CallbackAction::Stop => {
1582 success = true;
1583 message = "Optimization stopped by callback".to_string();
1584 break;
1585 }
1586 CallbackAction::Continue => {}
1587 }
1588 }
1589
1590 if pop_std <= convergence_threshold {
1591 success = true;
1592 message = format!(
1593 "Converged: std(pop_f)={:.3e} <= threshold={:.3e}",
1594 pop_std, convergence_threshold
1595 );
1596 break;
1597 }
1598 }
1599
1600 if !success {
1601 message = format!("Maximum iterations reached: {}", self.config.maxiter);
1602 }
1603
1604 if self.config.disp {
1605 eprintln!("DE finished: {}", message);
1606 }
1607
1608 let (final_x, final_f, polish_nfev) = if let Some(ref polish_cfg) = self.config.polish {
1610 if polish_cfg.enabled {
1611 self.polish(&best_x)
1612 } else {
1613 (best_x.clone(), best_f, 0)
1614 }
1615 } else {
1616 (best_x.clone(), best_f, 0)
1617 };
1618
1619 if timing_enabled {
1620 eprintln!(
1621 "TIMING total: build={:.3} s, eval={:.3} s, select={:.3} s, iter_total={:.3} s",
1622 t_build_tot.as_secs_f64(),
1623 t_eval_tot.as_secs_f64(),
1624 t_select_tot.as_secs_f64(),
1625 t_iter_tot.as_secs_f64()
1626 );
1627 }
1628
1629 self.finish_report(
1630 pop,
1631 energies,
1632 final_x,
1633 final_f,
1634 success,
1635 message,
1636 nit,
1637 nfev + polish_nfev,
1638 )
1639 }
1640}
1641
1642#[cfg(test)]
1643mod strategy_tests {
1644 use super::*;
1645
1646 #[test]
1647 fn test_parse_strategy_variants() {
1648 assert!(matches!(
1649 "best1exp".parse::<Strategy>().unwrap(),
1650 Strategy::Best1Exp
1651 ));
1652 assert!(matches!(
1653 "rand1bin".parse::<Strategy>().unwrap(),
1654 Strategy::Rand1Bin
1655 ));
1656 assert!(matches!(
1657 "randtobest1exp".parse::<Strategy>().unwrap(),
1658 Strategy::RandToBest1Exp
1659 ));
1660 }
1661}