1#![doc = include_str!("../README.md")]
75#![doc = include_str!("../REFERENCES.md")]
76#![warn(missing_docs)]
77
78pub mod error;
79pub mod levenberg_marquardt;
80pub use error::{DEError, Result};
81pub use levenberg_marquardt::{
82 LMCallbackAction, LMConfig, LMConfigBuilder, LMError, LMIntermediate, LMReport, LMResult,
83 levenberg_marquardt,
84};
85
86pub mod cobyla;
92mod cobyla_native;
93pub use cobyla::{CobylaConfig, CobylaConstraint, CobylaReport, CobylaStopTols};
94
95pub mod isres;
97pub use isres::{IsresConfig, IsresConstraint, IsresReport, isres};
98
99pub mod cmaes;
101pub use cmaes::{CmaEsConfig, CmaEsIntermediate, CmaEsReport, cma_es};
102
103pub mod nsga;
105pub use nsga::{NsgaConfig, NsgaReport, NsgaVariant, ParetoSolution, nsga, nsga2, nsga3};
106
107pub mod bayesian;
109
110pub mod continuous_area;
113pub use bayesian::{
114 BayesAcquisition, BayesOptCallback, BayesOptConfig, BayesOptIntermediate, BayesOptParetoReport,
115 BayesOptReport, BayesParetoSolution, bayesian_multi_objective, bayesian_optimization,
116};
117pub use continuous_area::{
118 AreaError, AreaScalarisation, Prior, Quadrature, build_quadrature_points, evaluate_area_loss,
119 try_evaluate_area_loss,
120};
121
122use std::fmt;
123use std::str::FromStr;
124use std::sync::Arc;
125use std::sync::RwLock;
126
127use ndarray::{Array1, Array2, Zip};
128use oxiblas_ndarray::blas::matvec;
129use rand::rngs::StdRng;
130use rand::{Rng, SeedableRng};
131use std::time::Instant;
132
133pub mod stack_linear_penalty;
135
136pub mod apply_integrality;
138pub mod apply_wls;
140
141pub mod distinct_indices;
143pub mod external_archive;
145pub mod init_latin_hypercube;
147pub mod init_random;
149pub mod init_sobol;
151pub mod lshade;
153
154pub mod mutant_adaptive;
156pub mod mutant_best1;
158pub mod mutant_best2;
160pub mod mutant_current_to_best1;
162pub mod mutant_current_to_pbest1;
164pub mod mutant_rand1;
166pub mod mutant_rand2;
168pub mod mutant_rand_to_best1;
170
171pub mod crossover_binomial;
173pub mod crossover_exponential;
175
176#[cfg(test)]
178mod de_tests;
179pub mod differential_evolution;
181pub mod function_registry;
183pub mod impl_helpers;
185pub mod metadata;
187pub mod parallel_eval;
189pub mod recorder;
191pub mod run_recorded;
193pub use differential_evolution::differential_evolution;
194pub use external_archive::ExternalArchive;
195pub use lshade::LShadeConfig;
196pub use parallel_eval::ParallelConfig;
197pub use recorder::{OptimizationRecord, OptimizationRecorder};
198pub use run_recorded::run_recorded_differential_evolution;
199
200pub type ScalarConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
203pub type VectorConstraintFn = Arc<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
205pub type PenaltyTuple = (ScalarConstraintFn, f64);
207pub type CallbackFn = Box<dyn FnMut(&DEIntermediate) -> CallbackAction>;
209
210mod argmin;
211pub(crate) use argmin::argmin;
212
213#[derive(Debug, Clone, Copy)]
220pub enum Strategy {
221 Best1Bin,
223 Best1Exp,
225 Rand1Bin,
227 Rand1Exp,
229 Rand2Bin,
231 Rand2Exp,
233 CurrentToBest1Bin,
235 CurrentToBest1Exp,
237 Best2Bin,
239 Best2Exp,
241 RandToBest1Bin,
243 RandToBest1Exp,
245 AdaptiveBin,
247 AdaptiveExp,
249 LShadeBin,
251 LShadeExp,
253}
254
255impl FromStr for Strategy {
256 type Err = String;
257 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
258 let t = s.to_lowercase();
259 match t.as_str() {
260 "best1bin" | "best1" => Ok(Strategy::Best1Bin),
261 "best1exp" => Ok(Strategy::Best1Exp),
262 "rand1bin" | "rand1" => Ok(Strategy::Rand1Bin),
263 "rand1exp" => Ok(Strategy::Rand1Exp),
264 "rand2bin" | "rand2" => Ok(Strategy::Rand2Bin),
265 "rand2exp" => Ok(Strategy::Rand2Exp),
266 "currenttobest1bin" | "current-to-best1bin" | "current_to_best1bin" => {
267 Ok(Strategy::CurrentToBest1Bin)
268 }
269 "currenttobest1exp" | "current-to-best1exp" | "current_to_best1exp" => {
270 Ok(Strategy::CurrentToBest1Exp)
271 }
272 "best2bin" | "best2" => Ok(Strategy::Best2Bin),
273 "best2exp" => Ok(Strategy::Best2Exp),
274 "randtobest1bin" | "rand-to-best1bin" | "rand_to_best1bin" => {
275 Ok(Strategy::RandToBest1Bin)
276 }
277 "randtobest1exp" | "rand-to-best1exp" | "rand_to_best1exp" => {
278 Ok(Strategy::RandToBest1Exp)
279 }
280 "adaptivebin" | "adaptive-bin" | "adaptive_bin" | "adaptive" => {
281 Ok(Strategy::AdaptiveBin)
282 }
283 "adaptiveexp" | "adaptive-exp" | "adaptive_exp" => Ok(Strategy::AdaptiveExp),
284 "lshadebin" | "lshade-bin" | "lshade_bin" | "lshade" => Ok(Strategy::LShadeBin),
285 "lshadeexp" | "lshade-exp" | "lshade_exp" => Ok(Strategy::LShadeExp),
286 _ => Err(format!("unknown strategy: {}", s)),
287 }
288 }
289}
290
291#[derive(Debug, Clone, Copy, Default)]
293pub enum Crossover {
294 #[default]
296 Binomial,
297 Exponential,
299}
300
301#[derive(Debug, Clone, Copy)]
303pub enum Mutation {
304 Factor(f64),
306 Range {
308 min: f64,
310 max: f64,
312 },
313 Adaptive {
315 initial_f: f64,
317 },
318}
319
320impl Default for Mutation {
321 fn default() -> Self {
322 let _ = Mutation::Factor(0.8);
323 Mutation::Range { min: 0.0, max: 2.0 }
324 }
325}
326
327impl Mutation {
328 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
329 match *self {
330 Mutation::Factor(f) => f,
331 Mutation::Range { min, max } => rng.random_range(min..max),
332 Mutation::Adaptive { initial_f } => initial_f, }
334 }
335
336 #[allow(dead_code)]
338 fn sample_cauchy<R: Rng + ?Sized>(&self, f_m: f64, _scale: f64, rng: &mut R) -> f64 {
339 let perturbation = (rng.random::<f64>() - 0.5) * 0.2; (f_m + perturbation).clamp(0.0, 2.0) }
343}
344
345#[derive(Debug, Clone, Copy, Default)]
347pub enum Init {
348 #[default]
350 LatinHypercube,
351 Random,
353}
354
355#[derive(Debug, Clone, Copy, Default)]
357pub enum Updating {
358 #[default]
360 Deferred,
361}
362
363#[derive(Debug, Clone)]
365pub struct LinearPenalty {
366 pub a: Array2<f64>,
368 pub lb: Array1<f64>,
370 pub ub: Array1<f64>,
372 pub weight: f64,
374}
375
376#[derive(Debug, Clone)]
378pub struct LinearConstraintHelper {
379 pub a: Array2<f64>,
381 pub lb: Array1<f64>,
383 pub ub: Array1<f64>,
385}
386
387impl LinearConstraintHelper {
388 pub fn apply_to(&self, cfg: &mut DEConfig, weight: f64) {
390 use stack_linear_penalty::stack_linear_penalty;
391
392 let new_lp = LinearPenalty {
393 a: self.a.clone(),
394 lb: self.lb.clone(),
395 ub: self.ub.clone(),
396 weight,
397 };
398 match &mut cfg.linear_penalty {
399 Some(existing) => stack_linear_penalty(existing, &new_lp),
400 None => cfg.linear_penalty = Some(new_lp),
401 }
402 }
403}
404
405#[derive(Clone)]
407pub struct NonlinearConstraintHelper {
408 pub fun: VectorConstraintFn,
410 pub lb: Array1<f64>,
412 pub ub: Array1<f64>,
414}
415
416impl NonlinearConstraintHelper {
417 pub fn apply_to(&self, cfg: &mut DEConfig, weight_ineq: f64, weight_eq: f64) {
421 let f = self.fun.clone();
422 let lb = self.lb.clone();
423 let ub = self.ub.clone();
424 let m = lb.len().min(ub.len());
425 for i in 0..m {
426 let l = lb[i];
427 let u = ub[i];
428 let tol = 1e-12 * (l.abs() + u.abs()).max(1.0);
429 if (u - l).abs() <= tol {
430 let fi = f.clone();
431 cfg.penalty_eq.push((
432 Arc::new(move |x: &Array1<f64>| {
433 let y = (fi)(x);
434 y[i] - l
435 }),
436 weight_eq,
437 ));
438 } else {
439 let fi_u = f.clone();
440 cfg.penalty_ineq.push((
441 Arc::new(move |x: &Array1<f64>| {
442 let y = (fi_u)(x);
443 y[i] - u
444 }),
445 weight_ineq,
446 ));
447 let fi_l = f.clone();
448 cfg.penalty_ineq.push((
449 Arc::new(move |x: &Array1<f64>| {
450 let y = (fi_l)(x);
451 l - y[i]
452 }),
453 weight_ineq,
454 ));
455 }
456 }
457 }
458}
459
460#[derive(Debug, Clone)]
462struct AdaptiveState {
463 f_m: f64,
465 cr_m: f64,
467 successful_f: Vec<f64>,
469 successful_cr: Vec<f64>,
471 current_w: f64,
473}
474
475impl AdaptiveState {
476 fn new(config: &AdaptiveConfig) -> Self {
477 Self {
478 f_m: config.f_m,
479 cr_m: config.cr_m,
480 successful_f: Vec::new(),
481 successful_cr: Vec::new(),
482 current_w: config.w_max, }
484 }
485
486 fn update(&mut self, config: &AdaptiveConfig, iter: usize, max_iter: usize) {
488 let iter_ratio = iter as f64 / max_iter as f64;
490 self.current_w = config.w_max - (config.w_max - config.w_min) * iter_ratio;
491
492 if !self.successful_f.is_empty() {
494 let mean_f = self.compute_lehmer_mean(&self.successful_f);
495 self.f_m = (1.0 - config.w_f) * self.f_m + config.w_f * mean_f;
496 self.f_m = self.f_m.clamp(0.2, 1.2);
497 }
498
499 if !self.successful_cr.is_empty() {
501 let mean_cr = self.compute_arithmetic_mean(&self.successful_cr);
502 self.cr_m = (1.0 - config.w_cr) * self.cr_m + config.w_cr * mean_cr;
503 self.cr_m = self.cr_m.clamp(0.1, 0.9);
504 }
505
506 self.successful_f.clear();
508 self.successful_cr.clear();
509 }
510
511 fn compute_lehmer_mean(&self, values: &[f64]) -> f64 {
513 if values.is_empty() {
514 return 0.5; }
516
517 let sum_sq: f64 = values.iter().map(|&x| x * x).sum();
518 let sum: f64 = values.iter().sum();
519
520 if sum > 0.0 {
521 sum_sq / sum
522 } else {
523 0.5 }
525 }
526
527 fn compute_arithmetic_mean(&self, values: &[f64]) -> f64 {
529 if values.is_empty() {
530 return 0.5; }
532 values.iter().sum::<f64>() / values.len() as f64
533 }
534
535 fn record_success(&mut self, f_val: f64, cr_val: f64) {
537 self.successful_f.push(f_val);
538 self.successful_cr.push(cr_val);
539 }
540
541 fn sample_f<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
544 let u = rng.random::<f64>().clamp(1e-10, 1.0 - 1e-10);
545 let cauchy = (std::f64::consts::PI * (u - 0.5)).tan();
546 let sample = self.f_m + 0.1 * cauchy;
547
548 sample.clamp(0.1, 1.5)
549 }
550
551 fn sample_cr<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
553 let u1: f64 = rng.random::<f64>().max(1e-15);
554 let u2: f64 = rng.random::<f64>();
555
556 let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
557 let sample = self.cr_m + 0.05 * normal;
558
559 sample.clamp(0.1, 0.9)
560 }
561}
562
563#[derive(Debug, Clone)]
565pub struct AdaptiveConfig {
566 pub adaptive_mutation: bool,
568 pub wls_enabled: bool,
570 pub w_max: f64,
572 pub w_min: f64,
574 pub w_f: f64,
576 pub w_cr: f64,
578 pub f_m: f64,
580 pub cr_m: f64,
582 pub wls_prob: f64,
584 pub wls_scale: f64,
586}
587
588impl Default for AdaptiveConfig {
589 fn default() -> Self {
590 Self {
591 adaptive_mutation: false,
592 wls_enabled: false,
593 w_max: 0.9,
594 w_min: 0.1,
595 w_f: 0.9,
596 w_cr: 0.9,
597 f_m: 0.5,
598 cr_m: 0.6,
599 wls_prob: 0.1,
600 wls_scale: 0.1,
601 }
602 }
603}
604
605#[derive(Debug, Clone)]
607pub struct PolishConfig {
608 pub enabled: bool,
610 pub maxeval: usize,
612}
613
614pub struct DEConfig {
620 pub maxiter: usize,
622 pub popsize: usize,
624 pub tol: f64,
626 pub atol: f64,
628 pub mutation: Mutation,
630 pub recombination: f64,
632 pub strategy: Strategy,
634 pub crossover: Crossover,
636 pub init: Init,
638 pub updating: Updating,
640 pub seed: Option<u64>,
642 pub integrality: Option<Vec<bool>>,
644 pub x0: Option<Array1<f64>>,
646 pub disp: bool,
648 pub callback: Option<CallbackFn>,
650 pub penalty_ineq: Vec<PenaltyTuple>,
652 pub penalty_eq: Vec<PenaltyTuple>,
654 pub linear_penalty: Option<LinearPenalty>,
656 pub polish: Option<PolishConfig>,
658 pub adaptive: AdaptiveConfig,
660 pub parallel: parallel_eval::ParallelConfig,
662 pub lshade: lshade::LShadeConfig,
664}
665
666impl Default for DEConfig {
667 fn default() -> Self {
668 Self {
669 maxiter: 1000,
670 popsize: 15,
671 tol: 1e-2,
672 atol: 0.0,
673 mutation: Mutation::default(),
674 recombination: 0.7,
675 strategy: Strategy::Best1Bin,
676 crossover: Crossover::default(),
677 init: Init::default(),
678 updating: Updating::default(),
679 seed: None,
680 integrality: None,
681 x0: None,
682 disp: false,
683 callback: None,
684 penalty_ineq: Vec::new(),
685 penalty_eq: Vec::new(),
686 linear_penalty: None,
687 polish: None,
688 adaptive: AdaptiveConfig::default(),
689 parallel: parallel_eval::ParallelConfig::default(),
690 lshade: lshade::LShadeConfig::default(),
691 }
692 }
693}
694
695pub struct DEConfigBuilder {
712 cfg: DEConfig,
713}
714impl Default for DEConfigBuilder {
715 fn default() -> Self {
716 Self::new()
717 }
718}
719
720impl DEConfigBuilder {
721 pub fn new() -> Self {
723 Self {
724 cfg: DEConfig::default(),
725 }
726 }
727 pub fn maxiter(mut self, v: usize) -> Self {
729 self.cfg.maxiter = v;
730 self
731 }
732 pub fn popsize(mut self, v: usize) -> Self {
739 self.cfg.popsize = v;
740 self
741 }
742 pub fn tol(mut self, v: f64) -> Self {
744 self.cfg.tol = v;
745 self
746 }
747 pub fn atol(mut self, v: f64) -> Self {
749 self.cfg.atol = v;
750 self
751 }
752 pub fn mutation(mut self, v: Mutation) -> Self {
754 self.cfg.mutation = v;
755 self
756 }
757 pub fn recombination(mut self, v: f64) -> Self {
759 self.cfg.recombination = v;
760 self
761 }
762 pub fn strategy(mut self, v: Strategy) -> Self {
764 self.cfg.strategy = v;
765 self
766 }
767 pub fn crossover(mut self, v: Crossover) -> Self {
769 self.cfg.crossover = v;
770 self
771 }
772 pub fn init(mut self, v: Init) -> Self {
774 self.cfg.init = v;
775 self
776 }
777 pub fn seed(mut self, v: u64) -> Self {
779 self.cfg.seed = Some(v);
780 self
781 }
782 pub fn integrality(mut self, v: Vec<bool>) -> Self {
784 self.cfg.integrality = Some(v);
785 self
786 }
787 pub fn x0(mut self, v: Array1<f64>) -> Self {
789 self.cfg.x0 = Some(v);
790 self
791 }
792 pub fn disp(mut self, v: bool) -> Self {
794 self.cfg.disp = v;
795 self
796 }
797 pub fn callback(mut self, cb: Box<dyn FnMut(&DEIntermediate) -> CallbackAction>) -> Self {
799 self.cfg.callback = Some(cb);
800 self
801 }
802 pub fn add_penalty_ineq<FN>(mut self, f: FN, w: f64) -> Self
804 where
805 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
806 {
807 self.cfg.penalty_ineq.push((Arc::new(f), w));
808 self
809 }
810 pub fn add_penalty_eq<FN>(mut self, f: FN, w: f64) -> Self
812 where
813 FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
814 {
815 self.cfg.penalty_eq.push((Arc::new(f), w));
816 self
817 }
818 pub fn linear_penalty(mut self, lp: LinearPenalty) -> Self {
820 self.cfg.linear_penalty = Some(lp);
821 self
822 }
823 pub fn polish(mut self, pol: PolishConfig) -> Self {
825 self.cfg.polish = Some(pol);
826 self
827 }
828 pub fn adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
830 self.cfg.adaptive = adaptive;
831 self
832 }
833 pub fn enable_adaptive_mutation(mut self, enable: bool) -> Self {
835 self.cfg.adaptive.adaptive_mutation = enable;
836 self
837 }
838 pub fn enable_wls(mut self, enable: bool) -> Self {
840 self.cfg.adaptive.wls_enabled = enable;
841 self
842 }
843 pub fn adaptive_weights(mut self, w_max: f64, w_min: f64) -> Self {
845 self.cfg.adaptive.w_max = w_max;
846 self.cfg.adaptive.w_min = w_min;
847 self
848 }
849 pub fn parallel(mut self, parallel: parallel_eval::ParallelConfig) -> Self {
851 self.cfg.parallel = parallel;
852 self
853 }
854 pub fn enable_parallel(mut self, enable: bool) -> Self {
856 self.cfg.parallel.enabled = enable;
857 self
858 }
859 pub fn parallel_threads(mut self, num_threads: usize) -> Self {
861 self.cfg.parallel.num_threads = Some(num_threads);
862 self
863 }
864 pub fn lshade(mut self, lshade: lshade::LShadeConfig) -> Self {
866 self.cfg.lshade = lshade;
867 self
868 }
869 pub fn build(self) -> error::Result<DEConfig> {
875 if self.cfg.popsize < 4 {
876 return Err(DEError::PopulationTooSmall {
877 pop_size: self.cfg.popsize,
878 });
879 }
880 Ok(self.cfg)
881 }
882}
883
884#[derive(Clone)]
888pub struct DEReport {
889 pub x: Array1<f64>,
891 pub fun: f64,
893 pub success: bool,
895 pub message: String,
897 pub nit: usize,
899 pub nfev: usize,
901 pub population: Array2<f64>,
903 pub population_energies: Array1<f64>,
905}
906
907impl fmt::Debug for DEReport {
908 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
909 f.debug_struct("DEReport")
910 .field("x", &format!("len={}", self.x.len()))
911 .field("fun", &self.fun)
912 .field("success", &self.success)
913 .field("message", &self.message)
914 .field("nit", &self.nit)
915 .field("nfev", &self.nfev)
916 .field(
917 "population",
918 &format!("{}x{}", self.population.nrows(), self.population.ncols()),
919 )
920 .field(
921 "population_energies",
922 &format!("len={}", self.population_energies.len()),
923 )
924 .finish()
925 }
926}
927
928pub struct DEIntermediate {
930 pub x: Array1<f64>,
932 pub fun: f64,
934 pub convergence: f64,
936 pub iter: usize,
938}
939
940pub enum CallbackAction {
942 Continue,
944 Stop,
946}
947
948pub struct DifferentialEvolution<'a, F>
954where
955 F: Fn(&Array1<f64>) -> f64 + Sync,
956{
957 func: &'a F,
958 lower: Array1<f64>,
959 upper: Array1<f64>,
960 config: DEConfig,
961}
962
963impl<'a, F> DifferentialEvolution<'a, F>
964where
965 F: Fn(&Array1<f64>) -> f64 + Sync,
966{
967 pub fn new(func: &'a F, lower: Array1<f64>, upper: Array1<f64>) -> Result<Self> {
974 if lower.len() != upper.len() {
975 return Err(DEError::BoundsMismatch {
976 lower_len: lower.len(),
977 upper_len: upper.len(),
978 });
979 }
980
981 for i in 0..lower.len() {
983 if lower[i] > upper[i] {
984 return Err(DEError::InvalidBounds {
985 index: i,
986 lower: lower[i],
987 upper: upper[i],
988 });
989 }
990 }
991
992 Ok(Self {
993 func,
994 lower,
995 upper,
996 config: DEConfig::default(),
997 })
998 }
999
1000 pub fn config_mut(&mut self) -> &mut DEConfig {
1002 &mut self.config
1003 }
1004
1005 pub fn solve(&mut self) -> DEReport {
1007 use apply_integrality::apply_integrality;
1008 use apply_wls::apply_wls;
1009 use crossover_binomial::binomial_crossover;
1010 use crossover_exponential::exponential_crossover;
1011 use init_latin_hypercube::init_latin_hypercube;
1012 use init_random::init_random;
1013 use mutant_adaptive::mutant_adaptive;
1014 use mutant_best1::mutant_best1;
1015 use mutant_best2::mutant_best2;
1016 use mutant_current_to_best1::mutant_current_to_best1;
1017 use mutant_current_to_pbest1;
1018 use mutant_rand_to_best1::mutant_rand_to_best1;
1019 use mutant_rand1::mutant_rand1;
1020 use mutant_rand2::mutant_rand2;
1021 use parallel_eval::evaluate_trials_parallel;
1022 use std::sync::Arc;
1023
1024 let n = self.lower.len();
1025
1026 let mut is_free: Vec<bool> = Vec::with_capacity(n);
1028 for i in 0..n {
1029 is_free.push((self.upper[i] - self.lower[i]).abs() > 0.0);
1030 }
1031 let n_free = is_free.iter().filter(|&&b| b).count();
1032 let _n_equal = n - n_free;
1033 if n_free == 0 {
1034 let x_fixed = self.lower.clone();
1036 let mut x_eval = x_fixed.clone();
1037 if let Some(mask) = &self.config.integrality {
1038 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1039 }
1040 let f = (self.func)(&x_eval);
1041 return DEReport {
1042 x: x_eval,
1043 fun: f,
1044 success: true,
1045 message: "All variables fixed by bounds".into(),
1046 nit: 0,
1047 nfev: 1,
1048 population: Array2::zeros((1, n)),
1049 population_energies: Array1::from(vec![f]),
1050 };
1051 }
1052
1053 let is_lshade = matches!(
1054 self.config.strategy,
1055 Strategy::LShadeBin | Strategy::LShadeExp
1056 );
1057 let mut npop = if is_lshade {
1058 self.config.lshade.initial_population_size(n_free)
1059 } else {
1060 self.config.popsize * n_free
1061 };
1062 let max_nfev = if is_lshade {
1063 self.config.maxiter * npop
1064 } else {
1065 0
1066 };
1067 let _bounds_span = &self.upper - &self.lower;
1068
1069 if self.config.disp {
1070 eprintln!(
1071 "DE Init: {} dimensions ({} free), population={}, maxiter={}",
1072 n, n_free, npop, self.config.maxiter
1073 );
1074 eprintln!(
1075 " Strategy: {:?}, Mutation: {:?}, Crossover: CR={:.3}",
1076 self.config.strategy, self.config.mutation, self.config.recombination
1077 );
1078 eprintln!(
1079 " Tolerances: tol={:.2e}, atol={:.2e}",
1080 self.config.tol, self.config.atol
1081 );
1082 }
1083
1084 let timing_enabled = std::env::var("AUTOEQ_DE_TIMING")
1086 .map(|v| v != "0")
1087 .unwrap_or(false);
1088
1089 if let Some(n) = self.config.parallel.num_threads {
1091 let _ = rayon::ThreadPoolBuilder::new()
1093 .num_threads(n)
1094 .build_global();
1095 }
1096
1097 let mut rng: StdRng = match self.config.seed {
1099 Some(s) => StdRng::seed_from_u64(s),
1100 None => {
1101 let mut thread_rng = rand::rng();
1102 StdRng::from_rng(&mut thread_rng)
1103 }
1104 };
1105
1106 let mut pop = match self.config.init {
1108 Init::LatinHypercube => {
1109 if self.config.disp {
1110 eprintln!(" Using Latin Hypercube initialization");
1111 }
1112 init_latin_hypercube(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1113 }
1114 Init::Random => {
1115 if self.config.disp {
1116 eprintln!(" Using Random initialization");
1117 }
1118 init_random(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1119 }
1120 };
1121
1122 let mut nfev: usize = 0;
1124 if self.config.disp {
1125 eprintln!(" Evaluating initial population of {} individuals...", npop);
1126 }
1127
1128 let mut eval_pop = pop.clone();
1130 let t_integrality0 = Instant::now();
1131 if let Some(mask) = &self.config.integrality {
1132 for i in 0..npop {
1133 let mut row = eval_pop.row_mut(i);
1134 let mut x_eval = row.to_owned();
1135 apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1136 row.assign(&x_eval);
1137 }
1138 }
1139 let t_integrality = t_integrality0.elapsed();
1140
1141 let func_ref = self.func;
1143 let penalty_ineq_vec: Vec<PenaltyTuple> = self
1144 .config
1145 .penalty_ineq
1146 .iter()
1147 .map(|(f, w)| (f.clone(), *w))
1148 .collect();
1149 let penalty_eq_vec: Vec<PenaltyTuple> = self
1150 .config
1151 .penalty_eq
1152 .iter()
1153 .map(|(f, w)| (f.clone(), *w))
1154 .collect();
1155 let linear_penalty = self.config.linear_penalty.clone();
1156
1157 let energy_fn = Arc::new(move |x: &Array1<f64>| -> f64 {
1158 let base = (func_ref)(x);
1159 let mut p = 0.0;
1160 for (f, w) in &penalty_ineq_vec {
1161 let v = f(x);
1162 let viol = v.max(0.0);
1163 p += w * viol * viol;
1164 }
1165 for (h, w) in &penalty_eq_vec {
1166 let v = h(x);
1167 p += w * v * v;
1168 }
1169 if let Some(ref lp) = linear_penalty {
1170 let ax = matvec(&lp.a, &x.to_owned());
1171 Zip::from(&ax)
1172 .and(&lp.lb)
1173 .and(&lp.ub)
1174 .for_each(|&v, &lo, &hi| {
1175 if v < lo {
1176 let d = lo - v;
1177 p += lp.weight * d * d;
1178 } else if v > hi {
1179 let d = v - hi;
1180 p += lp.weight * d * d;
1181 }
1182 });
1183 }
1184 let energy = base + p;
1185 if energy.is_finite() {
1186 energy
1187 } else {
1188 f64::INFINITY
1189 }
1190 });
1191
1192 let t_eval0 = Instant::now();
1193 let mut energies = parallel_eval::evaluate_population_parallel(
1194 &eval_pop,
1195 energy_fn.clone(),
1196 &self.config.parallel,
1197 );
1198 let t_eval_init = t_eval0.elapsed();
1199 nfev += npop;
1200 if timing_enabled {
1201 eprintln!(
1202 "TIMING init: integrality={:.3} ms, eval={:.3} ms",
1203 t_integrality.as_secs_f64() * 1e3,
1204 t_eval_init.as_secs_f64() * 1e3
1205 );
1206 }
1207
1208 let pop_mean = energies.mean().unwrap_or(0.0);
1210 let pop_std = energies.std(0.0);
1211 if self.config.disp {
1212 eprintln!(
1213 " Initial population: mean={:.6e}, std={:.6e}",
1214 pop_mean, pop_std
1215 );
1216 }
1217
1218 if let Some(x0) = &self.config.x0 {
1220 let mut x0c = x0.clone();
1221 for i in 0..x0c.len() {
1223 x0c[i] = x0c[i].clamp(self.lower[i], self.upper[i]);
1224 }
1225 if let Some(mask) = &self.config.integrality {
1226 apply_integrality(&mut x0c, mask, &self.lower, &self.upper);
1227 }
1228 let f0 = self.energy(&x0c);
1229 nfev += 1;
1230 let (best_idx, _best_f) = argmin(&energies);
1232 pop.row_mut(best_idx).assign(&x0c.view());
1233 energies[best_idx] = f0;
1234 }
1235
1236 let (mut best_idx, mut best_f) = argmin(&energies);
1237 let mut best_x = pop.row(best_idx).to_owned();
1238
1239 if self.config.disp {
1240 eprintln!(
1241 " Initial best: fitness={:.6e} at index {}",
1242 best_f, best_idx
1243 );
1244 let param_summary: Vec<String> = (0..best_x.len() / 3)
1245 .map(|i| {
1246 let freq = 10f64.powf(best_x[i * 3]);
1247 let q = best_x[i * 3 + 1];
1248 let gain = best_x[i * 3 + 2];
1249 format!("f{:.0}Hz/Q{:.2}/G{:.2}dB", freq, q, gain)
1250 })
1251 .collect();
1252 eprintln!(" Initial best params: [{}]", param_summary.join(", "));
1253 }
1254
1255 if self.config.disp {
1256 eprintln!("DE iter {:4} best_f={:.6e}", 0, best_f);
1257 }
1258
1259 let mut adaptive_state = if matches!(
1261 self.config.strategy,
1262 Strategy::AdaptiveBin | Strategy::AdaptiveExp
1263 ) || self.config.adaptive.adaptive_mutation
1264 {
1265 Some(AdaptiveState::new(&self.config.adaptive))
1266 } else {
1267 None
1268 };
1269
1270 let external_archive: Option<Arc<RwLock<external_archive::ExternalArchive>>> = if matches!(
1272 self.config.strategy,
1273 Strategy::LShadeBin | Strategy::LShadeExp
1274 ) {
1275 Some(Arc::new(RwLock::new(
1276 external_archive::ExternalArchive::with_population_size(
1277 npop,
1278 self.config.lshade.arc_rate,
1279 ),
1280 )))
1281 } else {
1282 None
1283 };
1284
1285 let mut success = false;
1287 let mut message = String::new();
1288 let mut nit = 0;
1289 let mut accepted_trials;
1290 let mut improvement_count;
1291
1292 let mut t_build_tot = std::time::Duration::ZERO;
1293 let mut t_eval_tot = std::time::Duration::ZERO;
1294 let mut t_select_tot = std::time::Duration::ZERO;
1295 let mut t_iter_tot = std::time::Duration::ZERO;
1296
1297 for iter in 1..=self.config.maxiter {
1298 nit = iter;
1299 accepted_trials = 0;
1300 improvement_count = 0;
1301
1302 let iter_start = Instant::now();
1303
1304 let sorted_indices = if matches!(
1306 self.config.strategy,
1307 Strategy::AdaptiveBin
1308 | Strategy::AdaptiveExp
1309 | Strategy::LShadeBin
1310 | Strategy::LShadeExp
1311 ) {
1312 let mut indices: Vec<usize> = (0..npop).collect();
1313 indices.sort_by(|&a, &b| {
1314 energies[a]
1315 .partial_cmp(&energies[b])
1316 .unwrap_or(std::cmp::Ordering::Equal)
1317 });
1318 indices
1319 } else {
1320 Vec::new() };
1322
1323 let t_build0 = Instant::now();
1325
1326 use rayon::prelude::*;
1328 let (trials, trial_params): (Vec<_>, Vec<_>) = (0..npop)
1329 .into_par_iter()
1330 .map(|i| {
1331 let mut local_rng: StdRng = if let Some(base_seed) = self.config.seed {
1333 StdRng::seed_from_u64(
1334 base_seed
1335 .wrapping_add((iter as u64) << 32)
1336 .wrapping_add(i as u64),
1337 )
1338 } else {
1339 let mut thread_rng = rand::rng();
1341 StdRng::from_rng(&mut thread_rng)
1342 };
1343
1344 let (f, cr) = if let Some(ref adaptive) = adaptive_state {
1346 let adaptive_f = adaptive.sample_f(&mut local_rng);
1348 let adaptive_cr = adaptive.sample_cr(&mut local_rng);
1349 (adaptive_f, adaptive_cr)
1350 } else {
1351 (
1353 self.config.mutation.sample(&mut local_rng),
1354 self.config.recombination,
1355 )
1356 };
1357
1358 let (mutant, cross) = match self.config.strategy {
1360 Strategy::Best1Bin => (
1361 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1362 Crossover::Binomial,
1363 ),
1364 Strategy::Best1Exp => (
1365 mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1366 Crossover::Exponential,
1367 ),
1368 Strategy::Rand1Bin => (
1369 mutant_rand1(i, &pop, f, &mut local_rng),
1370 Crossover::Binomial,
1371 ),
1372 Strategy::Rand1Exp => (
1373 mutant_rand1(i, &pop, f, &mut local_rng),
1374 Crossover::Exponential,
1375 ),
1376 Strategy::Rand2Bin => (
1377 mutant_rand2(i, &pop, f, &mut local_rng),
1378 Crossover::Binomial,
1379 ),
1380 Strategy::Rand2Exp => (
1381 mutant_rand2(i, &pop, f, &mut local_rng),
1382 Crossover::Exponential,
1383 ),
1384 Strategy::CurrentToBest1Bin => (
1385 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1386 Crossover::Binomial,
1387 ),
1388 Strategy::CurrentToBest1Exp => (
1389 mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1390 Crossover::Exponential,
1391 ),
1392 Strategy::Best2Bin => (
1393 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1394 Crossover::Binomial,
1395 ),
1396 Strategy::Best2Exp => (
1397 mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1398 Crossover::Exponential,
1399 ),
1400 Strategy::RandToBest1Bin => (
1401 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1402 Crossover::Binomial,
1403 ),
1404 Strategy::RandToBest1Exp => (
1405 mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1406 Crossover::Exponential,
1407 ),
1408 Strategy::AdaptiveBin => {
1409 if let Some(ref adaptive) = adaptive_state {
1410 (
1411 mutant_adaptive(
1412 i,
1413 &pop,
1414 &sorted_indices,
1415 adaptive.current_w,
1416 f,
1417 &mut local_rng,
1418 ),
1419 Crossover::Binomial,
1420 )
1421 } else {
1422 (
1424 mutant_rand1(i, &pop, f, &mut local_rng),
1425 Crossover::Binomial,
1426 )
1427 }
1428 }
1429 Strategy::AdaptiveExp => {
1430 if let Some(ref adaptive) = adaptive_state {
1431 (
1432 mutant_adaptive(
1433 i,
1434 &pop,
1435 &sorted_indices,
1436 adaptive.current_w,
1437 f,
1438 &mut local_rng,
1439 ),
1440 Crossover::Exponential,
1441 )
1442 } else {
1443 (
1445 mutant_rand1(i, &pop, f, &mut local_rng),
1446 Crossover::Exponential,
1447 )
1448 }
1449 }
1450 Strategy::LShadeBin => {
1451 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1452 self.config.lshade.p,
1453 pop.nrows(),
1454 );
1455 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1456 (
1457 mutant_current_to_pbest1::mutant_current_to_pbest1(
1458 i,
1459 &pop,
1460 &sorted_indices,
1461 pbest_size,
1462 archive_ref.as_deref(),
1463 f,
1464 &mut local_rng,
1465 ),
1466 Crossover::Binomial,
1467 )
1468 }
1469 Strategy::LShadeExp => {
1470 let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1471 self.config.lshade.p,
1472 pop.nrows(),
1473 );
1474 let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1475 (
1476 mutant_current_to_pbest1::mutant_current_to_pbest1(
1477 i,
1478 &pop,
1479 &sorted_indices,
1480 pbest_size,
1481 archive_ref.as_deref(),
1482 f,
1483 &mut local_rng,
1484 ),
1485 Crossover::Exponential,
1486 )
1487 }
1488 };
1489
1490 let crossover = cross;
1492 let trial = match crossover {
1493 Crossover::Binomial => {
1494 binomial_crossover(&pop.row(i).to_owned(), &mutant, cr, &mut local_rng)
1495 }
1496 Crossover::Exponential => exponential_crossover(
1497 &pop.row(i).to_owned(),
1498 &mutant,
1499 cr,
1500 &mut local_rng,
1501 ),
1502 };
1503
1504 let wls_trial = if self.config.adaptive.wls_enabled
1506 && local_rng.random::<f64>() < self.config.adaptive.wls_prob
1507 {
1508 apply_wls(
1509 &trial,
1510 &self.lower,
1511 &self.upper,
1512 self.config.adaptive.wls_scale,
1513 &mut local_rng,
1514 )
1515 } else {
1516 trial.clone()
1517 };
1518
1519 let mut trial_clipped = wls_trial;
1521 Zip::from(&mut trial_clipped)
1522 .and(&self.lower)
1523 .and(&self.upper)
1524 .for_each(|x, lo, hi| *x = x.clamp(*lo, *hi));
1525
1526 if let Some(mask) = &self.config.integrality {
1528 apply_integrality(&mut trial_clipped, mask, &self.lower, &self.upper);
1529 }
1530
1531 (trial_clipped, (f, cr))
1533 })
1534 .unzip();
1535
1536 let t_build = t_build0.elapsed();
1537 let t_eval0 = Instant::now();
1538 let trial_energies =
1539 evaluate_trials_parallel(&trials, energy_fn.clone(), &self.config.parallel);
1540 let t_eval = t_eval0.elapsed();
1541 nfev += npop;
1542
1543 let t_select0 = Instant::now();
1544 for (i, (trial, trial_energy)) in
1546 trials.into_iter().zip(trial_energies.iter()).enumerate()
1547 {
1548 let (f, cr) = trial_params[i];
1549
1550 if *trial_energy <= energies[i] {
1552 if let Some(ref archive) = external_archive
1554 && *trial_energy < energies[i]
1555 && let Ok(mut arch) = archive.write()
1556 {
1557 arch.add(pop.row(i).to_owned());
1558 }
1559 pop.row_mut(i).assign(&trial.view());
1560 energies[i] = *trial_energy;
1561 accepted_trials += 1;
1562
1563 if let Some(ref mut adaptive) = adaptive_state {
1565 adaptive.record_success(f, cr);
1566 }
1567
1568 if *trial_energy < best_f {
1570 improvement_count += 1;
1571 }
1572 }
1573 }
1574 let t_select = t_select0.elapsed();
1575
1576 if is_lshade {
1578 let current_npop = self
1579 .config
1580 .lshade
1581 .current_population_size(n_free, nfev, max_nfev);
1582 if current_npop < pop.nrows() {
1583 let mut indices: Vec<usize> = (0..pop.nrows()).collect();
1584 indices.sort_by(|&a, &b| {
1585 energies[a]
1586 .partial_cmp(&energies[b])
1587 .unwrap_or(std::cmp::Ordering::Equal)
1588 });
1589 let keep: Vec<usize> = indices.into_iter().take(current_npop).collect();
1590
1591 let mut new_pop = Array2::zeros((current_npop, n));
1592 let mut new_energies = Array1::zeros(current_npop);
1593 for (new_i, &old_i) in keep.iter().enumerate() {
1594 new_pop.row_mut(new_i).assign(&pop.row(old_i));
1595 new_energies[new_i] = energies[old_i];
1596 }
1597 pop = new_pop;
1598 energies = new_energies;
1599 npop = current_npop;
1600
1601 if let Some(ref archive) = external_archive
1602 && let Ok(mut arch) = archive.write()
1603 {
1604 arch.resize(self.config.lshade.current_archive_size(current_npop));
1605 }
1606 }
1607 }
1608
1609 t_build_tot += t_build;
1610 t_eval_tot += t_eval;
1611 t_select_tot += t_select;
1612 let iter_dur = iter_start.elapsed();
1613 t_iter_tot += iter_dur;
1614
1615 if timing_enabled && (iter <= 5 || iter % 10 == 0) {
1616 eprintln!(
1617 "TIMING iter {:4}: build={:.3} ms, eval={:.3} ms, select={:.3} ms, total={:.3} ms",
1618 iter,
1619 t_build.as_secs_f64() * 1e3,
1620 t_eval.as_secs_f64() * 1e3,
1621 t_select.as_secs_f64() * 1e3,
1622 iter_dur.as_secs_f64() * 1e3,
1623 );
1624 }
1625
1626 if let Some(ref mut adaptive) = adaptive_state {
1628 adaptive.update(&self.config.adaptive, iter, self.config.maxiter);
1629 }
1630
1631 let (new_best_idx, new_best_f) = argmin(&energies);
1633 if new_best_f < best_f {
1634 best_idx = new_best_idx;
1635 best_f = new_best_f;
1636 best_x = pop.row(best_idx).to_owned();
1637 }
1638
1639 let pop_mean = energies.mean().unwrap_or(0.0);
1641 let pop_std = energies.std(0.0);
1642 let convergence_threshold = self.config.atol + self.config.tol * pop_mean.abs();
1643
1644 if self.config.disp {
1645 eprintln!(
1646 "DE iter {:4} best_f={:.6e} std={:.3e} accepted={}/{}, improved={}",
1647 iter, best_f, pop_std, accepted_trials, npop, improvement_count
1648 );
1649 }
1650
1651 if let Some(ref mut cb) = self.config.callback {
1653 let intermediate = DEIntermediate {
1654 x: best_x.clone(),
1655 fun: best_f,
1656 convergence: pop_std,
1657 iter,
1658 };
1659 match cb(&intermediate) {
1660 CallbackAction::Stop => {
1661 success = true;
1662 message = "Optimization stopped by callback".to_string();
1663 break;
1664 }
1665 CallbackAction::Continue => {}
1666 }
1667 }
1668
1669 if pop_std <= convergence_threshold {
1670 success = true;
1671 message = format!(
1672 "Converged: std(pop_f)={:.3e} <= threshold={:.3e}",
1673 pop_std, convergence_threshold
1674 );
1675 break;
1676 }
1677 }
1678
1679 if !success {
1680 message = format!("Maximum iterations reached: {}", self.config.maxiter);
1681 }
1682
1683 if self.config.disp {
1684 eprintln!("DE finished: {}", message);
1685 }
1686
1687 let (final_x, final_f, polish_nfev) = if let Some(ref polish_cfg) = self.config.polish {
1689 if polish_cfg.enabled {
1690 self.polish(&best_x)
1691 } else {
1692 (best_x.clone(), best_f, 0)
1693 }
1694 } else {
1695 (best_x.clone(), best_f, 0)
1696 };
1697
1698 if timing_enabled {
1699 eprintln!(
1700 "TIMING total: build={:.3} s, eval={:.3} s, select={:.3} s, iter_total={:.3} s",
1701 t_build_tot.as_secs_f64(),
1702 t_eval_tot.as_secs_f64(),
1703 t_select_tot.as_secs_f64(),
1704 t_iter_tot.as_secs_f64()
1705 );
1706 }
1707
1708 self.finish_report(
1709 pop,
1710 energies,
1711 final_x,
1712 final_f,
1713 success,
1714 message,
1715 nit,
1716 nfev + polish_nfev,
1717 )
1718 }
1719}
1720
1721#[cfg(test)]
1722mod strategy_tests {
1723 use super::*;
1724 use rand::SeedableRng;
1725
1726 #[test]
1727 fn test_parse_strategy_variants() {
1728 assert!(matches!(
1729 "best1exp".parse::<Strategy>().unwrap(),
1730 Strategy::Best1Exp
1731 ));
1732 assert!(matches!(
1733 "rand1bin".parse::<Strategy>().unwrap(),
1734 Strategy::Rand1Bin
1735 ));
1736 assert!(matches!(
1737 "randtobest1exp".parse::<Strategy>().unwrap(),
1738 Strategy::RandToBest1Exp
1739 ));
1740 }
1741
1742 #[test]
1743 fn sample_f_uses_cauchy_heavy_tails() {
1744 let config = AdaptiveConfig::default();
1749 let state = AdaptiveState::new(&config);
1750 let mut rng = StdRng::seed_from_u64(42);
1751 let mut extreme = 0;
1752 for _ in 0..200 {
1753 let f = state.sample_f(&mut rng);
1754 if !(0.1..=0.9).contains(&f) {
1755 extreme += 1;
1756 }
1757 }
1758 assert!(
1759 extreme > 5,
1760 "Cauchy sampling should produce heavy tails, got {} extreme values",
1761 extreme
1762 );
1763 }
1764}