Skip to main content

math_audio_optimisation/
mod.rs

1//! Differential Evolution optimization library.
2//!
3//! This crate provides a Rust implementation of the Differential Evolution (DE)
4//! algorithm, a population-based stochastic optimizer for continuous optimization
5//! problems. The implementation is inspired by SciPy's differential_evolution.
6//!
7//! # Features
8//!
9//! - Multiple mutation strategies (Best1, Rand1, CurrentToBest1, etc.)
10//! - Binomial and exponential crossover
11//! - Adaptive parameter control (SHADE-style)
12//! - **L-SHADE**: Linear population size reduction for faster convergence
13//! - **External Archive**: Maintains diversity by storing discarded solutions
14//! - **Current-to-pbest/1**: SHADE mutation strategy for balanced exploration
15//! - Parallel population evaluation
16//! - Constraint handling via penalty methods
17//! - Latin Hypercube initialization
18//!
19//! # Example
20//!
21//! ```rust
22//! use math_audio_optimisation::{differential_evolution, DEConfigBuilder};
23//!
24//! // Minimize the sphere function: f(x) = sum(x_i^2)
25//! let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
26//! let config = DEConfigBuilder::new()
27//!     .maxiter(100)
28//!     .seed(42)
29//!     .build()
30//!     .expect("invalid config");
31//!
32//! let result = differential_evolution(
33//!     &|x| x.iter().map(|&xi| xi * xi).sum(),
34//!     &bounds,
35//!     config,
36//! ).expect("optimization should succeed");
37//!
38//! assert!(result.fun < 1e-6);
39//! ```
40//!
41//! # L-SHADE Example
42//!
43//! ```rust
44//! use math_audio_optimisation::{differential_evolution, DEConfigBuilder, Strategy, LShadeConfig};
45//!
46//! let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
47//! let lshade_config = LShadeConfig {
48//!     np_init: 18,
49//!     np_final: 4,
50//!     p: 0.11,
51//!     arc_rate: 2.1,
52//!     ..Default::default()
53//! };
54//!
55//! let config = DEConfigBuilder::new()
56//!     .maxiter(100)
57//!     .strategy(Strategy::LShadeBin)
58//!     .lshade(lshade_config)
59//!     .seed(42)
60//!     .build()
61//!     .expect("invalid config");
62//!
63//! let result = differential_evolution(
64//!     &|x| x.iter().map(|&xi| xi * xi).sum(),
65//!     &bounds,
66//!     config,
67//! ).expect("optimization should succeed");
68//!
69//! assert!(result.fun < 1e-6);
70//! ```
71#![doc = include_str!("../README.md")]
72#![doc = include_str!("../REFERENCES.md")]
73#![warn(missing_docs)]
74
75pub mod error;
76pub mod levenberg_marquardt;
77pub use error::{DEError, Result};
78pub use levenberg_marquardt::{
79    LMCallbackAction, LMConfig, LMConfigBuilder, LMError, LMIntermediate, LMReport, LMResult,
80    levenberg_marquardt,
81};
82
83use std::fmt;
84use std::str::FromStr;
85use std::sync::Arc;
86use std::sync::RwLock;
87
88use ndarray::{Array1, Array2, Zip};
89use oxiblas_ndarray::blas::matvec;
90use rand::rngs::StdRng;
91use rand::{Rng, SeedableRng};
92use std::time::Instant;
93
94/// Linear penalty stacking utilities for combining multiple constraints.
95pub mod stack_linear_penalty;
96
97/// Integer variable handling for mixed-integer optimization.
98pub mod apply_integrality;
99/// Wrapper Local Search (WLS) application for local refinement.
100pub mod apply_wls;
101
102/// Utilities for selecting distinct random indices from a population.
103pub mod distinct_indices;
104/// External archive for L-SHADE algorithm.
105pub mod external_archive;
106/// Latin Hypercube Sampling initialization strategy.
107pub mod init_latin_hypercube;
108/// Random uniform initialization strategy.
109pub mod init_random;
110/// Sobol quasi-random initialization strategy.
111pub mod init_sobol;
112/// L-SHADE configuration.
113pub mod lshade;
114
115/// Adaptive mutation strategy with dynamic parameter control.
116pub mod mutant_adaptive;
117/// Best/1 mutation strategy: uses best individual plus one difference vector.
118pub mod mutant_best1;
119/// Best/2 mutation strategy: uses best individual plus two difference vectors.
120pub mod mutant_best2;
121/// Current-to-best/1 mutation: blends current with best individual.
122pub mod mutant_current_to_best1;
123/// Current-to-pbest/1 mutation: blends current with p-best individual (SHADE).
124pub mod mutant_current_to_pbest1;
125/// Rand/1 mutation strategy: uses random individual plus one difference vector.
126pub mod mutant_rand1;
127/// Rand/2 mutation strategy: uses random individual plus two difference vectors.
128pub mod mutant_rand2;
129/// Rand-to-best/1 mutation: blends random with best individual.
130pub mod mutant_rand_to_best1;
131
132/// Binomial (uniform) crossover implementation.
133pub mod crossover_binomial;
134/// Exponential crossover implementation.
135pub mod crossover_exponential;
136
137/// Comprehensive tests for DE strategies and features.
138#[cfg(test)]
139mod de_tests;
140/// Main differential evolution algorithm implementation.
141pub mod differential_evolution;
142/// Registry of standard test functions for benchmarking.
143pub mod function_registry;
144/// Internal helper functions for DE implementation.
145pub mod impl_helpers;
146/// Metadata-driven optimization examples and tests.
147pub mod metadata;
148/// Parallel population evaluation support.
149pub mod parallel_eval;
150/// Optimization recording for analysis and debugging.
151pub mod recorder;
152/// Recorded optimization wrapper for testing.
153pub mod run_recorded;
154pub use differential_evolution::differential_evolution;
155pub use external_archive::ExternalArchive;
156pub use lshade::LShadeConfig;
157pub use parallel_eval::ParallelConfig;
158pub use recorder::{OptimizationRecord, OptimizationRecorder};
159pub use run_recorded::run_recorded_differential_evolution;
160
161// Type aliases to reduce complexity
162/// Scalar constraint function type
163pub type ScalarConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
164/// Vector constraint function type
165pub type VectorConstraintFn = Arc<dyn Fn(&Array1<f64>) -> Array1<f64> + Send + Sync>;
166/// Penalty tuple type (function, weight)
167pub type PenaltyTuple = (ScalarConstraintFn, f64);
168/// Callback function type
169pub type CallbackFn = Box<dyn FnMut(&DEIntermediate) -> CallbackAction>;
170
171pub(crate) fn argmin(v: &Array1<f64>) -> (usize, f64) {
172    let mut best_i = 0usize;
173    let mut best_v = v[0];
174    for (i, &val) in v.iter().enumerate() {
175        if val < best_v {
176            best_v = val;
177            best_i = i;
178        }
179    }
180    (best_i, best_v)
181}
182
183/// Differential Evolution mutation/crossover strategy.
184///
185/// The strategy name follows the pattern `{mutation}{n}{crossover}` where:
186/// - `mutation`: Base vector selection (Best, Rand, CurrentToBest, RandToBest, Adaptive)
187/// - `n`: Number of difference vectors (1 or 2)
188/// - `crossover`: Crossover type (Bin = binomial, Exp = exponential)
189#[derive(Debug, Clone, Copy)]
190pub enum Strategy {
191    /// Best/1/Bin: Best individual + 1 difference vector, binomial crossover
192    Best1Bin,
193    /// Best/1/Exp: Best individual + 1 difference vector, exponential crossover
194    Best1Exp,
195    /// Rand/1/Bin: Random individual + 1 difference vector, binomial crossover
196    Rand1Bin,
197    /// Rand/1/Exp: Random individual + 1 difference vector, exponential crossover
198    Rand1Exp,
199    /// Rand/2/Bin: Random individual + 2 difference vectors, binomial crossover
200    Rand2Bin,
201    /// Rand/2/Exp: Random individual + 2 difference vectors, exponential crossover
202    Rand2Exp,
203    /// Current-to-best/1/Bin: Blend of current and best + 1 diff, binomial crossover
204    CurrentToBest1Bin,
205    /// Current-to-best/1/Exp: Blend of current and best + 1 diff, exponential crossover
206    CurrentToBest1Exp,
207    /// Best/2/Bin: Best individual + 2 difference vectors, binomial crossover
208    Best2Bin,
209    /// Best/2/Exp: Best individual + 2 difference vectors, exponential crossover
210    Best2Exp,
211    /// Rand-to-best/1/Bin: Blend of random and best + 1 diff, binomial crossover
212    RandToBest1Bin,
213    /// Rand-to-best/1/Exp: Blend of random and best + 1 diff, exponential crossover
214    RandToBest1Exp,
215    /// Adaptive mutation with binomial crossover (SAM approach)
216    AdaptiveBin,
217    /// Adaptive mutation with exponential crossover
218    AdaptiveExp,
219    /// L-SHADE with binomial crossover: current-to-pbest/1 + linear pop reduction + archive
220    LShadeBin,
221    /// L-SHADE with exponential crossover: current-to-pbest/1 + linear pop reduction + archive
222    LShadeExp,
223}
224
225impl FromStr for Strategy {
226    type Err = String;
227    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
228        let t = s.to_lowercase();
229        match t.as_str() {
230            "best1bin" | "best1" => Ok(Strategy::Best1Bin),
231            "best1exp" => Ok(Strategy::Best1Exp),
232            "rand1bin" | "rand1" => Ok(Strategy::Rand1Bin),
233            "rand1exp" => Ok(Strategy::Rand1Exp),
234            "rand2bin" | "rand2" => Ok(Strategy::Rand2Bin),
235            "rand2exp" => Ok(Strategy::Rand2Exp),
236            "currenttobest1bin" | "current-to-best1bin" | "current_to_best1bin" => {
237                Ok(Strategy::CurrentToBest1Bin)
238            }
239            "currenttobest1exp" | "current-to-best1exp" | "current_to_best1exp" => {
240                Ok(Strategy::CurrentToBest1Exp)
241            }
242            "best2bin" | "best2" => Ok(Strategy::Best2Bin),
243            "best2exp" => Ok(Strategy::Best2Exp),
244            "randtobest1bin" | "rand-to-best1bin" | "rand_to_best1bin" => {
245                Ok(Strategy::RandToBest1Bin)
246            }
247            "randtobest1exp" | "rand-to-best1exp" | "rand_to_best1exp" => {
248                Ok(Strategy::RandToBest1Exp)
249            }
250            "adaptivebin" | "adaptive-bin" | "adaptive_bin" | "adaptive" => {
251                Ok(Strategy::AdaptiveBin)
252            }
253            "adaptiveexp" | "adaptive-exp" | "adaptive_exp" => Ok(Strategy::AdaptiveExp),
254            "lshadebin" | "lshade-bin" | "lshade_bin" | "lshade" => Ok(Strategy::LShadeBin),
255            "lshadeexp" | "lshade-exp" | "lshade_exp" => Ok(Strategy::LShadeExp),
256            _ => Err(format!("unknown strategy: {}", s)),
257        }
258    }
259}
260
261/// Crossover type
262#[derive(Debug, Clone, Copy, Default)]
263pub enum Crossover {
264    /// Binomial (uniform) crossover
265    #[default]
266    Binomial,
267    /// Exponential crossover
268    Exponential,
269}
270
271/// Mutation setting: either a fixed factor, a uniform range (dithering), or adaptive.
272#[derive(Debug, Clone, Copy)]
273pub enum Mutation {
274    /// Fixed mutation factor F in [0, 2).
275    Factor(f64),
276    /// Dithering range [min, max) with 0 <= min < max <= 2.
277    Range {
278        /// Minimum mutation factor.
279        min: f64,
280        /// Maximum mutation factor.
281        max: f64,
282    },
283    /// Adaptive mutation factor using Cauchy distribution.
284    Adaptive {
285        /// Initial mutation factor before adaptation.
286        initial_f: f64,
287    },
288}
289
290impl Default for Mutation {
291    fn default() -> Self {
292        let _ = Mutation::Factor(0.8);
293        Mutation::Range { min: 0.0, max: 2.0 }
294    }
295}
296
297impl Mutation {
298    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
299        match *self {
300            Mutation::Factor(f) => f,
301            Mutation::Range { min, max } => rng.random_range(min..max),
302            Mutation::Adaptive { initial_f } => initial_f, // Will be overridden by adaptive logic
303        }
304    }
305
306    /// Sample from Cauchy distribution for adaptive mutation (F parameter)
307    #[allow(dead_code)]
308    fn sample_cauchy<R: Rng + ?Sized>(&self, f_m: f64, _scale: f64, rng: &mut R) -> f64 {
309        // Simplified version using normal random for now
310        let perturbation = (rng.random::<f64>() - 0.5) * 0.2; // Small perturbation
311        (f_m + perturbation).clamp(0.0, 2.0) // Clamp to valid range
312    }
313}
314
315/// Initialization scheme for the population.
316#[derive(Debug, Clone, Copy, Default)]
317pub enum Init {
318    /// Latin Hypercube Sampling for better space coverage.
319    #[default]
320    LatinHypercube,
321    /// Uniform random initialization.
322    Random,
323}
324
325/// Whether best updates during a generation (we use Deferred only).
326#[derive(Debug, Clone, Copy, Default)]
327pub enum Updating {
328    /// Deferred update: best is updated after all trials are evaluated.
329    #[default]
330    Deferred,
331}
332
333/// Linear penalty specification: lb <= A x <= ub (component-wise).
334#[derive(Debug, Clone)]
335pub struct LinearPenalty {
336    /// Constraint matrix A (m x n).
337    pub a: Array2<f64>,
338    /// Lower bounds vector (m elements).
339    pub lb: Array1<f64>,
340    /// Upper bounds vector (m elements).
341    pub ub: Array1<f64>,
342    /// Penalty weight for constraint violations.
343    pub weight: f64,
344}
345
346/// SciPy-like linear constraint helper: lb <= A x <= ub.
347#[derive(Debug, Clone)]
348pub struct LinearConstraintHelper {
349    /// Constraint matrix A (m x n).
350    pub a: Array2<f64>,
351    /// Lower bounds vector (m elements).
352    pub lb: Array1<f64>,
353    /// Upper bounds vector (m elements).
354    pub ub: Array1<f64>,
355}
356
357impl LinearConstraintHelper {
358    /// Apply helper by merging into DEConfig.linear_penalty (stacking rows if already present)
359    pub fn apply_to(&self, cfg: &mut DEConfig, weight: f64) {
360        use stack_linear_penalty::stack_linear_penalty;
361
362        let new_lp = LinearPenalty {
363            a: self.a.clone(),
364            lb: self.lb.clone(),
365            ub: self.ub.clone(),
366            weight,
367        };
368        match &mut cfg.linear_penalty {
369            Some(existing) => stack_linear_penalty(existing, &new_lp),
370            None => cfg.linear_penalty = Some(new_lp),
371        }
372    }
373}
374
375/// SciPy-like nonlinear constraint helper: vector-valued fun(x) with lb <= fun(x) <= ub.
376#[derive(Clone)]
377pub struct NonlinearConstraintHelper {
378    /// Vector-valued constraint function.
379    pub fun: VectorConstraintFn,
380    /// Lower bounds for each constraint component.
381    pub lb: Array1<f64>,
382    /// Upper bounds for each constraint component.
383    pub ub: Array1<f64>,
384}
385
386impl NonlinearConstraintHelper {
387    /// Apply helper by emitting penalty closures per component.
388    /// lb <= f_i(x) <= ub becomes two inequalities: f_i(x)-ub <= 0 and lb - f_i(x) <= 0.
389    /// If lb==ub, emit an equality penalty for f_i(x)-lb.
390    pub fn apply_to(&self, cfg: &mut DEConfig, weight_ineq: f64, weight_eq: f64) {
391        let f = self.fun.clone();
392        let lb = self.lb.clone();
393        let ub = self.ub.clone();
394        let m = lb.len().min(ub.len());
395        for i in 0..m {
396            let l = lb[i];
397            let u = ub[i];
398            let tol = 1e-12 * (l.abs() + u.abs()).max(1.0);
399            if (u - l).abs() <= tol {
400                let fi = f.clone();
401                cfg.penalty_eq.push((
402                    Arc::new(move |x: &Array1<f64>| {
403                        let y = (fi)(x);
404                        y[i] - l
405                    }),
406                    weight_eq,
407                ));
408            } else {
409                let fi_u = f.clone();
410                cfg.penalty_ineq.push((
411                    Arc::new(move |x: &Array1<f64>| {
412                        let y = (fi_u)(x);
413                        y[i] - u
414                    }),
415                    weight_ineq,
416                ));
417                let fi_l = f.clone();
418                cfg.penalty_ineq.push((
419                    Arc::new(move |x: &Array1<f64>| {
420                        let y = (fi_l)(x);
421                        l - y[i]
422                    }),
423                    weight_ineq,
424                ));
425            }
426        }
427    }
428}
429
430/// Structures for tracking adaptive parameters
431#[derive(Debug, Clone)]
432struct AdaptiveState {
433    /// Current F_m parameter for Cauchy distribution (mutation)
434    f_m: f64,
435    /// Current CR_m parameter for Gaussian distribution (crossover)
436    cr_m: f64,
437    /// Successful F values from this generation
438    successful_f: Vec<f64>,
439    /// Successful CR values from this generation
440    successful_cr: Vec<f64>,
441    /// Current linearly decreasing weight for adaptive mutation
442    current_w: f64,
443}
444
445impl AdaptiveState {
446    fn new(config: &AdaptiveConfig) -> Self {
447        Self {
448            f_m: config.f_m,
449            cr_m: config.cr_m,
450            successful_f: Vec::new(),
451            successful_cr: Vec::new(),
452            current_w: config.w_max, // Start with maximum weight
453        }
454    }
455
456    /// Update adaptive parameters based on successful trials
457    fn update(&mut self, config: &AdaptiveConfig, iter: usize, max_iter: usize) {
458        // Update linearly decreasing weight (Equation 19 from the paper)
459        let iter_ratio = iter as f64 / max_iter as f64;
460        self.current_w = config.w_max - (config.w_max - config.w_min) * iter_ratio;
461
462        // Update F_m using Lehmer mean of successful F values
463        if !self.successful_f.is_empty() {
464            let mean_f = self.compute_lehmer_mean(&self.successful_f);
465            self.f_m = (1.0 - config.w_f) * self.f_m + config.w_f * mean_f;
466            self.f_m = self.f_m.clamp(0.2, 1.2);
467        }
468
469        // Update CR_m using arithmetic mean of successful CR values
470        if !self.successful_cr.is_empty() {
471            let mean_cr = self.compute_arithmetic_mean(&self.successful_cr);
472            self.cr_m = (1.0 - config.w_cr) * self.cr_m + config.w_cr * mean_cr;
473            self.cr_m = self.cr_m.clamp(0.1, 0.9);
474        }
475
476        // Clear successful values for next generation
477        self.successful_f.clear();
478        self.successful_cr.clear();
479    }
480
481    /// Compute Lehmer mean for successful F values (p=2)
482    fn compute_lehmer_mean(&self, values: &[f64]) -> f64 {
483        if values.is_empty() {
484            return 0.5; // Default fallback
485        }
486
487        let sum_sq: f64 = values.iter().map(|&x| x * x).sum();
488        let sum: f64 = values.iter().sum();
489
490        if sum > 0.0 {
491            sum_sq / sum
492        } else {
493            0.5 // Fallback if sum is zero
494        }
495    }
496
497    /// Compute arithmetic mean for successful CR values
498    fn compute_arithmetic_mean(&self, values: &[f64]) -> f64 {
499        if values.is_empty() {
500            return 0.5; // Default fallback
501        }
502        values.iter().sum::<f64>() / values.len() as f64
503    }
504
505    /// Record successful parameter values
506    fn record_success(&mut self, f_val: f64, cr_val: f64) {
507        self.successful_f.push(f_val);
508        self.successful_cr.push(cr_val);
509    }
510
511    /// Sample adaptive F parameter using conservative normal distribution
512    fn sample_f<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
513        let u1: f64 = rng.random::<f64>().max(1e-15);
514        let u2: f64 = rng.random::<f64>();
515
516        let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
517        let sample = self.f_m + 0.05 * normal;
518
519        sample.clamp(0.3, 1.0)
520    }
521
522    /// Sample adaptive CR parameter using conservative Gaussian distribution
523    fn sample_cr<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
524        let u1: f64 = rng.random::<f64>().max(1e-15);
525        let u2: f64 = rng.random::<f64>();
526
527        let normal = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
528        let sample = self.cr_m + 0.05 * normal;
529
530        sample.clamp(0.1, 0.9)
531    }
532}
533
534/// Adaptive differential evolution configuration
535#[derive(Debug, Clone)]
536pub struct AdaptiveConfig {
537    /// Enable adaptive mutation strategy
538    pub adaptive_mutation: bool,
539    /// Enable Wrapper Local Search (WLS)
540    pub wls_enabled: bool,
541    /// Maximum weight for adaptive mutation (w_max)
542    pub w_max: f64,
543    /// Minimum weight for adaptive mutation (w_min)
544    pub w_min: f64,
545    /// Weight factor for F parameter adaptation (between 0.8 and 1.0)
546    pub w_f: f64,
547    /// Weight factor for CR parameter adaptation (between 0.9 and 1.0)
548    pub w_cr: f64,
549    /// Initial location parameter for Cauchy distribution (F_m)
550    pub f_m: f64,
551    /// Initial location parameter for Gaussian distribution (CR_m)
552    pub cr_m: f64,
553    /// WLS probability (what fraction of population to apply WLS to)
554    pub wls_prob: f64,
555    /// WLS Cauchy scale parameter
556    pub wls_scale: f64,
557}
558
559impl Default for AdaptiveConfig {
560    fn default() -> Self {
561        Self {
562            adaptive_mutation: false,
563            wls_enabled: false,
564            w_max: 0.9,
565            w_min: 0.1,
566            w_f: 0.9,
567            w_cr: 0.9,
568            f_m: 0.5,
569            cr_m: 0.6,
570            wls_prob: 0.1,
571            wls_scale: 0.1,
572        }
573    }
574}
575
576/// Polishing configuration using NLopt local optimizer within bounds.
577#[derive(Debug, Clone)]
578pub struct PolishConfig {
579    /// Whether polishing is enabled.
580    pub enabled: bool,
581    /// Local optimizer algorithm name (e.g., "neldermead", "sbplx", "cobyla").
582    pub algo: String,
583    /// Maximum function evaluations for polishing (e.g., 200*n).
584    pub maxeval: usize,
585}
586
587/// Configuration for the Differential Evolution optimizer.
588///
589/// This struct holds all parameters controlling the DE algorithm behavior,
590/// including population size, mutation/crossover settings, constraints, and
591/// convergence criteria.
592pub struct DEConfig {
593    /// Maximum number of generations (iterations).
594    pub maxiter: usize,
595    /// Population size multiplier (total NP = popsize * n_params_free).
596    pub popsize: usize,
597    /// Relative tolerance for convergence (population energy std dev).
598    pub tol: f64,
599    /// Absolute tolerance for convergence on best fitness.
600    pub atol: f64,
601    /// Mutation factor setting.
602    pub mutation: Mutation,
603    /// Crossover probability CR in [0, 1].
604    pub recombination: f64,
605    /// Mutation/crossover strategy.
606    pub strategy: Strategy,
607    /// Crossover type (binomial or exponential).
608    pub crossover: Crossover,
609    /// Population initialization scheme.
610    pub init: Init,
611    /// Update timing (deferred).
612    pub updating: Updating,
613    /// Optional random seed for reproducibility.
614    pub seed: Option<u64>,
615    /// Optional integrality mask; true => variable is integer-constrained.
616    pub integrality: Option<Vec<bool>>,
617    /// Optional initial guess used to replace the best member after init.
618    pub x0: Option<Array1<f64>>,
619    /// Print objective best at each iteration.
620    pub disp: bool,
621    /// Optional per-iteration callback (may stop early).
622    pub callback: Option<CallbackFn>,
623    /// Penalty-based inequality constraints: fc(x) <= 0.
624    pub penalty_ineq: Vec<PenaltyTuple>,
625    /// Penalty-based equality constraints: h(x) = 0.
626    pub penalty_eq: Vec<PenaltyTuple>,
627    /// Optional linear constraints treated by penalty: lb <= A x <= ub.
628    pub linear_penalty: Option<LinearPenalty>,
629    /// Polishing configuration (optional).
630    pub polish: Option<PolishConfig>,
631    /// Adaptive differential evolution configuration.
632    pub adaptive: AdaptiveConfig,
633    /// Parallel evaluation configuration.
634    pub parallel: parallel_eval::ParallelConfig,
635    /// L-SHADE configuration for population reduction and pbest selection.
636    pub lshade: lshade::LShadeConfig,
637}
638
639impl Default for DEConfig {
640    fn default() -> Self {
641        Self {
642            maxiter: 1000,
643            popsize: 15,
644            tol: 1e-2,
645            atol: 0.0,
646            mutation: Mutation::default(),
647            recombination: 0.7,
648            strategy: Strategy::Best1Bin,
649            crossover: Crossover::default(),
650            init: Init::default(),
651            updating: Updating::default(),
652            seed: None,
653            integrality: None,
654            x0: None,
655            disp: false,
656            callback: None,
657            penalty_ineq: Vec::new(),
658            penalty_eq: Vec::new(),
659            linear_penalty: None,
660            polish: None,
661            adaptive: AdaptiveConfig::default(),
662            parallel: parallel_eval::ParallelConfig::default(),
663            lshade: lshade::LShadeConfig::default(),
664        }
665    }
666}
667
668/// Fluent builder for `DEConfig` for ergonomic configuration.
669///
670/// # Example
671///
672/// ```rust
673/// use math_audio_optimisation::{DEConfigBuilder, Strategy, Mutation};
674///
675/// let config = DEConfigBuilder::new()
676///     .maxiter(500)
677///     .popsize(20)
678///     .strategy(Strategy::Best1Bin)
679///     .mutation(Mutation::Factor(0.8))
680///     .recombination(0.9)
681///     .seed(42)
682///     .build();
683/// ```
684pub struct DEConfigBuilder {
685    cfg: DEConfig,
686}
687impl Default for DEConfigBuilder {
688    fn default() -> Self {
689        Self::new()
690    }
691}
692
693impl DEConfigBuilder {
694    /// Creates a new builder with default configuration.
695    pub fn new() -> Self {
696        Self {
697            cfg: DEConfig::default(),
698        }
699    }
700    /// Sets the maximum number of iterations.
701    pub fn maxiter(mut self, v: usize) -> Self {
702        self.cfg.maxiter = v;
703        self
704    }
705    /// Sets the population size multiplier.
706    ///
707    /// # Panics
708    ///
709    /// Panics if `v < 4` since DE requires at least 4 individuals for
710    /// rand/1 and rand/2 mutation strategies.
711    pub fn popsize(mut self, v: usize) -> Self {
712        self.cfg.popsize = v;
713        self
714    }
715    /// Sets the relative convergence tolerance.
716    pub fn tol(mut self, v: f64) -> Self {
717        self.cfg.tol = v;
718        self
719    }
720    /// Sets the absolute convergence tolerance.
721    pub fn atol(mut self, v: f64) -> Self {
722        self.cfg.atol = v;
723        self
724    }
725    /// Sets the mutation factor configuration.
726    pub fn mutation(mut self, v: Mutation) -> Self {
727        self.cfg.mutation = v;
728        self
729    }
730    /// Sets the crossover probability (CR).
731    pub fn recombination(mut self, v: f64) -> Self {
732        self.cfg.recombination = v;
733        self
734    }
735    /// Sets the mutation/crossover strategy.
736    pub fn strategy(mut self, v: Strategy) -> Self {
737        self.cfg.strategy = v;
738        self
739    }
740    /// Sets the crossover type.
741    pub fn crossover(mut self, v: Crossover) -> Self {
742        self.cfg.crossover = v;
743        self
744    }
745    /// Sets the population initialization scheme.
746    pub fn init(mut self, v: Init) -> Self {
747        self.cfg.init = v;
748        self
749    }
750    /// Sets the random seed for reproducibility.
751    pub fn seed(mut self, v: u64) -> Self {
752        self.cfg.seed = Some(v);
753        self
754    }
755    /// Sets the integrality mask for mixed-integer optimization.
756    pub fn integrality(mut self, v: Vec<bool>) -> Self {
757        self.cfg.integrality = Some(v);
758        self
759    }
760    /// Sets an initial guess to seed the population.
761    pub fn x0(mut self, v: Array1<f64>) -> Self {
762        self.cfg.x0 = Some(v);
763        self
764    }
765    /// Enables/disables progress display.
766    pub fn disp(mut self, v: bool) -> Self {
767        self.cfg.disp = v;
768        self
769    }
770    /// Sets a per-iteration callback function.
771    pub fn callback(mut self, cb: Box<dyn FnMut(&DEIntermediate) -> CallbackAction>) -> Self {
772        self.cfg.callback = Some(cb);
773        self
774    }
775    /// Adds an inequality constraint penalty function.
776    pub fn add_penalty_ineq<FN>(mut self, f: FN, w: f64) -> Self
777    where
778        FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
779    {
780        self.cfg.penalty_ineq.push((Arc::new(f), w));
781        self
782    }
783    /// Adds an equality constraint penalty function.
784    pub fn add_penalty_eq<FN>(mut self, f: FN, w: f64) -> Self
785    where
786        FN: Fn(&Array1<f64>) -> f64 + Send + Sync + 'static,
787    {
788        self.cfg.penalty_eq.push((Arc::new(f), w));
789        self
790    }
791    /// Sets a linear constraint penalty.
792    pub fn linear_penalty(mut self, lp: LinearPenalty) -> Self {
793        self.cfg.linear_penalty = Some(lp);
794        self
795    }
796    /// Sets the polishing configuration.
797    pub fn polish(mut self, pol: PolishConfig) -> Self {
798        self.cfg.polish = Some(pol);
799        self
800    }
801    /// Sets the adaptive DE configuration.
802    pub fn adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
803        self.cfg.adaptive = adaptive;
804        self
805    }
806    /// Enables/disables adaptive mutation.
807    pub fn enable_adaptive_mutation(mut self, enable: bool) -> Self {
808        self.cfg.adaptive.adaptive_mutation = enable;
809        self
810    }
811    /// Enables/disables Wrapper Local Search.
812    pub fn enable_wls(mut self, enable: bool) -> Self {
813        self.cfg.adaptive.wls_enabled = enable;
814        self
815    }
816    /// Sets the adaptive weight bounds.
817    pub fn adaptive_weights(mut self, w_max: f64, w_min: f64) -> Self {
818        self.cfg.adaptive.w_max = w_max;
819        self.cfg.adaptive.w_min = w_min;
820        self
821    }
822    /// Sets the parallel evaluation configuration.
823    pub fn parallel(mut self, parallel: parallel_eval::ParallelConfig) -> Self {
824        self.cfg.parallel = parallel;
825        self
826    }
827    /// Enables/disables parallel evaluation.
828    pub fn enable_parallel(mut self, enable: bool) -> Self {
829        self.cfg.parallel.enabled = enable;
830        self
831    }
832    /// Sets the number of parallel threads.
833    pub fn parallel_threads(mut self, num_threads: usize) -> Self {
834        self.cfg.parallel.num_threads = Some(num_threads);
835        self
836    }
837    /// Sets the L-SHADE configuration.
838    pub fn lshade(mut self, lshade: lshade::LShadeConfig) -> Self {
839        self.cfg.lshade = lshade;
840        self
841    }
842    /// Builds and returns the configuration.
843    ///
844    /// # Errors
845    ///
846    /// Returns `DEError::PopulationTooSmall` if `popsize < 4`.
847    pub fn build(self) -> error::Result<DEConfig> {
848        if self.cfg.popsize < 4 {
849            return Err(DEError::PopulationTooSmall {
850                pop_size: self.cfg.popsize,
851            });
852        }
853        Ok(self.cfg)
854    }
855}
856
857/// Result/report of a DE optimization run.
858///
859/// Contains the optimal solution, convergence status, and statistics.
860#[derive(Clone)]
861pub struct DEReport {
862    /// The optimal solution vector.
863    pub x: Array1<f64>,
864    /// The objective function value at the optimal solution.
865    pub fun: f64,
866    /// Whether the optimization converged successfully.
867    pub success: bool,
868    /// Human-readable status message.
869    pub message: String,
870    /// Number of iterations (generations) performed.
871    pub nit: usize,
872    /// Number of function evaluations performed.
873    pub nfev: usize,
874    /// Final population matrix (NP x n).
875    pub population: Array2<f64>,
876    /// Fitness values for each population member.
877    pub population_energies: Array1<f64>,
878}
879
880impl fmt::Debug for DEReport {
881    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
882        f.debug_struct("DEReport")
883            .field("x", &format!("len={}", self.x.len()))
884            .field("fun", &self.fun)
885            .field("success", &self.success)
886            .field("message", &self.message)
887            .field("nit", &self.nit)
888            .field("nfev", &self.nfev)
889            .field(
890                "population",
891                &format!("{}x{}", self.population.nrows(), self.population.ncols()),
892            )
893            .field(
894                "population_energies",
895                &format!("len={}", self.population_energies.len()),
896            )
897            .finish()
898    }
899}
900
901/// Information passed to callback after each generation.
902pub struct DEIntermediate {
903    /// Current best solution vector.
904    pub x: Array1<f64>,
905    /// Current best objective value.
906    pub fun: f64,
907    /// Convergence measure (population fitness std dev).
908    pub convergence: f64,
909    /// Current iteration number.
910    pub iter: usize,
911}
912
913/// Action returned by callback to control optimization flow.
914pub enum CallbackAction {
915    /// Continue optimization.
916    Continue,
917    /// Stop optimization early.
918    Stop,
919}
920
921/// Differential Evolution optimizer.
922///
923/// A population-based stochastic optimizer for continuous functions.
924/// Use [`DifferentialEvolution::new`] to create an instance, configure
925/// with [`config_mut`](Self::config_mut), then call [`solve`](Self::solve).
926pub struct DifferentialEvolution<'a, F>
927where
928    F: Fn(&Array1<f64>) -> f64 + Sync,
929{
930    func: &'a F,
931    lower: Array1<f64>,
932    upper: Array1<f64>,
933    config: DEConfig,
934}
935
936impl<'a, F> DifferentialEvolution<'a, F>
937where
938    F: Fn(&Array1<f64>) -> f64 + Sync,
939{
940    /// Creates a new DE optimizer with objective `func` and bounds [lower, upper].
941    ///
942    /// # Errors
943    ///
944    /// Returns `DEError::BoundsMismatch` if `lower` and `upper` have different lengths.
945    /// Returns `DEError::InvalidBounds` if any lower bound exceeds its corresponding upper bound.
946    pub fn new(func: &'a F, lower: Array1<f64>, upper: Array1<f64>) -> Result<Self> {
947        if lower.len() != upper.len() {
948            return Err(DEError::BoundsMismatch {
949                lower_len: lower.len(),
950                upper_len: upper.len(),
951            });
952        }
953
954        // Validate that lower <= upper for all dimensions
955        for i in 0..lower.len() {
956            if lower[i] > upper[i] {
957                return Err(DEError::InvalidBounds {
958                    index: i,
959                    lower: lower[i],
960                    upper: upper[i],
961                });
962            }
963        }
964
965        Ok(Self {
966            func,
967            lower,
968            upper,
969            config: DEConfig::default(),
970        })
971    }
972
973    /// Mutable access to configuration
974    pub fn config_mut(&mut self) -> &mut DEConfig {
975        &mut self.config
976    }
977
978    /// Run the optimization and return a report
979    pub fn solve(&mut self) -> DEReport {
980        use apply_integrality::apply_integrality;
981        use apply_wls::apply_wls;
982        use crossover_binomial::binomial_crossover;
983        use crossover_exponential::exponential_crossover;
984        use init_latin_hypercube::init_latin_hypercube;
985        use init_random::init_random;
986        use mutant_adaptive::mutant_adaptive;
987        use mutant_best1::mutant_best1;
988        use mutant_best2::mutant_best2;
989        use mutant_current_to_best1::mutant_current_to_best1;
990        use mutant_current_to_pbest1;
991        use mutant_rand_to_best1::mutant_rand_to_best1;
992        use mutant_rand1::mutant_rand1;
993        use mutant_rand2::mutant_rand2;
994        use parallel_eval::evaluate_trials_parallel;
995        use std::sync::Arc;
996
997        let n = self.lower.len();
998
999        // Identify fixed (equal-bounds) and free variables
1000        let mut is_free: Vec<bool> = Vec::with_capacity(n);
1001        for i in 0..n {
1002            is_free.push((self.upper[i] - self.lower[i]).abs() > 0.0);
1003        }
1004        let n_free = is_free.iter().filter(|&&b| b).count();
1005        let _n_equal = n - n_free;
1006        if n_free == 0 {
1007            // All fixed; just evaluate x = lower
1008            let x_fixed = self.lower.clone();
1009            let mut x_eval = x_fixed.clone();
1010            if let Some(mask) = &self.config.integrality {
1011                apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1012            }
1013            let f = (self.func)(&x_eval);
1014            return DEReport {
1015                x: x_eval,
1016                fun: f,
1017                success: true,
1018                message: "All variables fixed by bounds".into(),
1019                nit: 0,
1020                nfev: 1,
1021                population: Array2::zeros((1, n)),
1022                population_energies: Array1::from(vec![f]),
1023            };
1024        }
1025
1026        let npop = self.config.popsize * n_free;
1027        let _bounds_span = &self.upper - &self.lower;
1028
1029        if self.config.disp {
1030            eprintln!(
1031                "DE Init: {} dimensions ({} free), population={}, maxiter={}",
1032                n, n_free, npop, self.config.maxiter
1033            );
1034            eprintln!(
1035                "  Strategy: {:?}, Mutation: {:?}, Crossover: CR={:.3}",
1036                self.config.strategy, self.config.mutation, self.config.recombination
1037            );
1038            eprintln!(
1039                "  Tolerances: tol={:.2e}, atol={:.2e}",
1040                self.config.tol, self.config.atol
1041            );
1042        }
1043
1044        // Timing toggle via env var
1045        let timing_enabled = std::env::var("AUTOEQ_DE_TIMING")
1046            .map(|v| v != "0")
1047            .unwrap_or(false);
1048
1049        // Configure global rayon thread pool once if requested
1050        if let Some(n) = self.config.parallel.num_threads {
1051            // Ignore error if global pool already set
1052            let _ = rayon::ThreadPoolBuilder::new()
1053                .num_threads(n)
1054                .build_global();
1055        }
1056
1057        // RNG
1058        let mut rng: StdRng = match self.config.seed {
1059            Some(s) => StdRng::seed_from_u64(s),
1060            None => {
1061                let mut thread_rng = rand::rng();
1062                StdRng::from_rng(&mut thread_rng)
1063            }
1064        };
1065
1066        // Initialize population in [lower, upper]
1067        let mut pop = match self.config.init {
1068            Init::LatinHypercube => {
1069                if self.config.disp {
1070                    eprintln!("  Using Latin Hypercube initialization");
1071                }
1072                init_latin_hypercube(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1073            }
1074            Init::Random => {
1075                if self.config.disp {
1076                    eprintln!("  Using Random initialization");
1077                }
1078                init_random(n, npop, &self.lower, &self.upper, &is_free, &mut rng)
1079            }
1080        };
1081
1082        // Evaluate energies (objective + penalties)
1083        let mut nfev: usize = 0;
1084        if self.config.disp {
1085            eprintln!("  Evaluating initial population of {} individuals...", npop);
1086        }
1087
1088        // Prepare population for evaluation (apply integrality constraints)
1089        let mut eval_pop = pop.clone();
1090        let t_integrality0 = Instant::now();
1091        if let Some(mask) = &self.config.integrality {
1092            for i in 0..npop {
1093                let mut row = eval_pop.row_mut(i);
1094                let mut x_eval = row.to_owned();
1095                apply_integrality(&mut x_eval, mask, &self.lower, &self.upper);
1096                row.assign(&x_eval);
1097            }
1098        }
1099        let t_integrality = t_integrality0.elapsed();
1100
1101        // Build thread-safe energy function that includes penalties
1102        let func_ref = self.func;
1103        let penalty_ineq_vec: Vec<PenaltyTuple> = self
1104            .config
1105            .penalty_ineq
1106            .iter()
1107            .map(|(f, w)| (f.clone(), *w))
1108            .collect();
1109        let penalty_eq_vec: Vec<PenaltyTuple> = self
1110            .config
1111            .penalty_eq
1112            .iter()
1113            .map(|(f, w)| (f.clone(), *w))
1114            .collect();
1115        let linear_penalty = self.config.linear_penalty.clone();
1116
1117        let energy_fn = Arc::new(move |x: &Array1<f64>| -> f64 {
1118            let base = (func_ref)(x);
1119            let mut p = 0.0;
1120            for (f, w) in &penalty_ineq_vec {
1121                let v = f(x);
1122                let viol = v.max(0.0);
1123                p += w * viol * viol;
1124            }
1125            for (h, w) in &penalty_eq_vec {
1126                let v = h(x);
1127                p += w * v * v;
1128            }
1129            if let Some(ref lp) = linear_penalty {
1130                let ax = matvec(&lp.a, &x.to_owned());
1131                Zip::from(&ax)
1132                    .and(&lp.lb)
1133                    .and(&lp.ub)
1134                    .for_each(|&v, &lo, &hi| {
1135                        if v < lo {
1136                            let d = lo - v;
1137                            p += lp.weight * d * d;
1138                        } else if v > hi {
1139                            let d = v - hi;
1140                            p += lp.weight * d * d;
1141                        }
1142                    });
1143            }
1144            base + p
1145        });
1146
1147        let t_eval0 = Instant::now();
1148        let mut energies = parallel_eval::evaluate_population_parallel(
1149            &eval_pop,
1150            energy_fn.clone(),
1151            &self.config.parallel,
1152        );
1153        let t_eval_init = t_eval0.elapsed();
1154        nfev += npop;
1155        if timing_enabled {
1156            eprintln!(
1157                "TIMING init: integrality={:.3} ms, eval={:.3} ms",
1158                t_integrality.as_secs_f64() * 1e3,
1159                t_eval_init.as_secs_f64() * 1e3
1160            );
1161        }
1162
1163        // Report initial population statistics
1164        let pop_mean = energies.mean().unwrap_or(0.0);
1165        let pop_std = energies.std(0.0);
1166        if self.config.disp {
1167            eprintln!(
1168                "  Initial population: mean={:.6e}, std={:.6e}",
1169                pop_mean, pop_std
1170            );
1171        }
1172
1173        // If x0 provided, override the best member
1174        if let Some(x0) = &self.config.x0 {
1175            let mut x0c = x0.clone();
1176            // Clip to bounds using ndarray
1177            for i in 0..x0c.len() {
1178                x0c[i] = x0c[i].clamp(self.lower[i], self.upper[i]);
1179            }
1180            if let Some(mask) = &self.config.integrality {
1181                apply_integrality(&mut x0c, mask, &self.lower, &self.upper);
1182            }
1183            let f0 = self.energy(&x0c);
1184            nfev += 1;
1185            // find current best
1186            let (best_idx, _best_f) = argmin(&energies);
1187            pop.row_mut(best_idx).assign(&x0c.view());
1188            energies[best_idx] = f0;
1189        }
1190
1191        let (mut best_idx, mut best_f) = argmin(&energies);
1192        let mut best_x = pop.row(best_idx).to_owned();
1193
1194        if self.config.disp {
1195            eprintln!(
1196                "  Initial best: fitness={:.6e} at index {}",
1197                best_f, best_idx
1198            );
1199            let param_summary: Vec<String> = (0..best_x.len() / 3)
1200                .map(|i| {
1201                    let freq = 10f64.powf(best_x[i * 3]);
1202                    let q = best_x[i * 3 + 1];
1203                    let gain = best_x[i * 3 + 2];
1204                    format!("f{:.0}Hz/Q{:.2}/G{:.2}dB", freq, q, gain)
1205                })
1206                .collect();
1207            eprintln!("  Initial best params: [{}]", param_summary.join(", "));
1208        }
1209
1210        if self.config.disp {
1211            eprintln!("DE iter {:4}  best_f={:.6e}", 0, best_f);
1212        }
1213
1214        // Initialize adaptive state if adaptive strategies are enabled
1215        let mut adaptive_state = if matches!(
1216            self.config.strategy,
1217            Strategy::AdaptiveBin | Strategy::AdaptiveExp
1218        ) || self.config.adaptive.adaptive_mutation
1219        {
1220            Some(AdaptiveState::new(&self.config.adaptive))
1221        } else {
1222            None
1223        };
1224
1225        // Initialize external archive for L-SHADE strategies
1226        let external_archive: Option<Arc<RwLock<external_archive::ExternalArchive>>> = if matches!(
1227            self.config.strategy,
1228            Strategy::LShadeBin | Strategy::LShadeExp
1229        ) {
1230            Some(Arc::new(RwLock::new(
1231                external_archive::ExternalArchive::with_population_size(
1232                    npop,
1233                    self.config.lshade.arc_rate,
1234                ),
1235            )))
1236        } else {
1237            None
1238        };
1239
1240        // Main loop
1241        let mut success = false;
1242        let mut message = String::new();
1243        let mut nit = 0;
1244        let mut accepted_trials;
1245        let mut improvement_count;
1246
1247        let mut t_build_tot = std::time::Duration::ZERO;
1248        let mut t_eval_tot = std::time::Duration::ZERO;
1249        let mut t_select_tot = std::time::Duration::ZERO;
1250        let mut t_iter_tot = std::time::Duration::ZERO;
1251
1252        for iter in 1..=self.config.maxiter {
1253            nit = iter;
1254            accepted_trials = 0;
1255            improvement_count = 0;
1256
1257            let iter_start = Instant::now();
1258
1259            // Pre-sort indices for adaptive/L-SHADE strategies to avoid re-sorting in the loop
1260            let sorted_indices = if matches!(
1261                self.config.strategy,
1262                Strategy::AdaptiveBin
1263                    | Strategy::AdaptiveExp
1264                    | Strategy::LShadeBin
1265                    | Strategy::LShadeExp
1266            ) {
1267                let mut indices: Vec<usize> = (0..npop).collect();
1268                indices.sort_by(|&a, &b| {
1269                    energies[a]
1270                        .partial_cmp(&energies[b])
1271                        .unwrap_or(std::cmp::Ordering::Equal)
1272                });
1273                indices
1274            } else {
1275                Vec::new() // Not needed for other strategies
1276            };
1277
1278            // Generate all trials first, then evaluate in parallel
1279            let t_build0 = Instant::now();
1280
1281            // Parallelize trial generation using rayon
1282            use rayon::prelude::*;
1283            let (trials, trial_params): (Vec<_>, Vec<_>) = (0..npop)
1284                .into_par_iter()
1285                .map(|i| {
1286                    // Create thread-local RNG from base seed + iteration + individual index
1287                    let mut local_rng: StdRng = if let Some(base_seed) = self.config.seed {
1288                        StdRng::seed_from_u64(
1289                            base_seed
1290                                .wrapping_add((iter as u64) << 32)
1291                                .wrapping_add(i as u64),
1292                        )
1293                    } else {
1294                        // Use thread_rng for unseeded runs
1295                        let mut thread_rng = rand::rng();
1296                        StdRng::from_rng(&mut thread_rng)
1297                    };
1298
1299                    // Sample mutation factor and crossover rate (adaptive or fixed)
1300                    let (f, cr) = if let Some(ref adaptive) = adaptive_state {
1301                        // Use adaptive parameter sampling
1302                        let adaptive_f = adaptive.sample_f(&mut local_rng);
1303                        let adaptive_cr = adaptive.sample_cr(&mut local_rng);
1304                        (adaptive_f, adaptive_cr)
1305                    } else {
1306                        // Use fixed or dithered parameters
1307                        (
1308                            self.config.mutation.sample(&mut local_rng),
1309                            self.config.recombination,
1310                        )
1311                    };
1312
1313                    // Generate mutant and apply crossover based on strategy
1314                    let (mutant, cross) = match self.config.strategy {
1315                        Strategy::Best1Bin => (
1316                            mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1317                            Crossover::Binomial,
1318                        ),
1319                        Strategy::Best1Exp => (
1320                            mutant_best1(i, &pop, best_idx, f, &mut local_rng),
1321                            Crossover::Exponential,
1322                        ),
1323                        Strategy::Rand1Bin => (
1324                            mutant_rand1(i, &pop, f, &mut local_rng),
1325                            Crossover::Binomial,
1326                        ),
1327                        Strategy::Rand1Exp => (
1328                            mutant_rand1(i, &pop, f, &mut local_rng),
1329                            Crossover::Exponential,
1330                        ),
1331                        Strategy::Rand2Bin => (
1332                            mutant_rand2(i, &pop, f, &mut local_rng),
1333                            Crossover::Binomial,
1334                        ),
1335                        Strategy::Rand2Exp => (
1336                            mutant_rand2(i, &pop, f, &mut local_rng),
1337                            Crossover::Exponential,
1338                        ),
1339                        Strategy::CurrentToBest1Bin => (
1340                            mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1341                            Crossover::Binomial,
1342                        ),
1343                        Strategy::CurrentToBest1Exp => (
1344                            mutant_current_to_best1(i, &pop, best_idx, f, &mut local_rng),
1345                            Crossover::Exponential,
1346                        ),
1347                        Strategy::Best2Bin => (
1348                            mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1349                            Crossover::Binomial,
1350                        ),
1351                        Strategy::Best2Exp => (
1352                            mutant_best2(i, &pop, best_idx, f, &mut local_rng),
1353                            Crossover::Exponential,
1354                        ),
1355                        Strategy::RandToBest1Bin => (
1356                            mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1357                            Crossover::Binomial,
1358                        ),
1359                        Strategy::RandToBest1Exp => (
1360                            mutant_rand_to_best1(i, &pop, best_idx, f, &mut local_rng),
1361                            Crossover::Exponential,
1362                        ),
1363                        Strategy::AdaptiveBin => {
1364                            if let Some(ref adaptive) = adaptive_state {
1365                                (
1366                                    mutant_adaptive(
1367                                        i,
1368                                        &pop,
1369                                        &sorted_indices,
1370                                        adaptive.current_w,
1371                                        f,
1372                                        &mut local_rng,
1373                                    ),
1374                                    Crossover::Binomial,
1375                                )
1376                            } else {
1377                                // Fallback to rand1 if adaptive state not available
1378                                (
1379                                    mutant_rand1(i, &pop, f, &mut local_rng),
1380                                    Crossover::Binomial,
1381                                )
1382                            }
1383                        }
1384                        Strategy::AdaptiveExp => {
1385                            if let Some(ref adaptive) = adaptive_state {
1386                                (
1387                                    mutant_adaptive(
1388                                        i,
1389                                        &pop,
1390                                        &sorted_indices,
1391                                        adaptive.current_w,
1392                                        f,
1393                                        &mut local_rng,
1394                                    ),
1395                                    Crossover::Exponential,
1396                                )
1397                            } else {
1398                                // Fallback to rand1 if adaptive state not available
1399                                (
1400                                    mutant_rand1(i, &pop, f, &mut local_rng),
1401                                    Crossover::Exponential,
1402                                )
1403                            }
1404                        }
1405                        Strategy::LShadeBin => {
1406                            let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1407                                self.config.lshade.p,
1408                                pop.nrows(),
1409                            );
1410                            let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1411                            (
1412                                mutant_current_to_pbest1::mutant_current_to_pbest1(
1413                                    i,
1414                                    &pop,
1415                                    &sorted_indices,
1416                                    pbest_size,
1417                                    archive_ref.as_deref(),
1418                                    f,
1419                                    &mut local_rng,
1420                                ),
1421                                Crossover::Binomial,
1422                            )
1423                        }
1424                        Strategy::LShadeExp => {
1425                            let pbest_size = mutant_current_to_pbest1::compute_pbest_size(
1426                                self.config.lshade.p,
1427                                pop.nrows(),
1428                            );
1429                            let archive_ref = external_archive.as_ref().and_then(|a| a.read().ok());
1430                            (
1431                                mutant_current_to_pbest1::mutant_current_to_pbest1(
1432                                    i,
1433                                    &pop,
1434                                    &sorted_indices,
1435                                    pbest_size,
1436                                    archive_ref.as_deref(),
1437                                    f,
1438                                    &mut local_rng,
1439                                ),
1440                                Crossover::Exponential,
1441                            )
1442                        }
1443                    };
1444
1445                    // If strategy didn't dictate crossover, fallback to config
1446                    let crossover = cross;
1447                    let trial = match crossover {
1448                        Crossover::Binomial => {
1449                            binomial_crossover(&pop.row(i).to_owned(), &mutant, cr, &mut local_rng)
1450                        }
1451                        Crossover::Exponential => exponential_crossover(
1452                            &pop.row(i).to_owned(),
1453                            &mutant,
1454                            cr,
1455                            &mut local_rng,
1456                        ),
1457                    };
1458
1459                    // Apply WLS if enabled
1460                    let wls_trial = if self.config.adaptive.wls_enabled
1461                        && local_rng.random::<f64>() < self.config.adaptive.wls_prob
1462                    {
1463                        apply_wls(
1464                            &trial,
1465                            &self.lower,
1466                            &self.upper,
1467                            self.config.adaptive.wls_scale,
1468                            &mut local_rng,
1469                        )
1470                    } else {
1471                        trial.clone()
1472                    };
1473
1474                    // Clip to bounds using vectorized operation
1475                    let mut trial_clipped = wls_trial;
1476                    Zip::from(&mut trial_clipped)
1477                        .and(&self.lower)
1478                        .and(&self.upper)
1479                        .for_each(|x, lo, hi| *x = x.clamp(*lo, *hi));
1480
1481                    // Apply integrality if provided
1482                    if let Some(mask) = &self.config.integrality {
1483                        apply_integrality(&mut trial_clipped, mask, &self.lower, &self.upper);
1484                    }
1485
1486                    // Return trial and parameters
1487                    (trial_clipped, (f, cr))
1488                })
1489                .unzip();
1490
1491            let t_build = t_build0.elapsed();
1492            let t_eval0 = Instant::now();
1493            let trial_energies =
1494                evaluate_trials_parallel(&trials, energy_fn.clone(), &self.config.parallel);
1495            let t_eval = t_eval0.elapsed();
1496            nfev += npop;
1497
1498            let t_select0 = Instant::now();
1499            // Selection phase: update population based on trial results
1500            for (i, (trial, trial_energy)) in
1501                trials.into_iter().zip(trial_energies.iter()).enumerate()
1502            {
1503                let (f, cr) = trial_params[i];
1504
1505                // Selection: replace if better
1506                if *trial_energy <= energies[i] {
1507                    // For L-SHADE: add replaced solution to archive
1508                    if let Some(ref archive) = external_archive
1509                        && *trial_energy < energies[i]
1510                        && let Ok(mut arch) = archive.write()
1511                    {
1512                        arch.add(pop.row(i).to_owned());
1513                    }
1514                    pop.row_mut(i).assign(&trial.view());
1515                    energies[i] = *trial_energy;
1516                    accepted_trials += 1;
1517
1518                    // Update adaptive parameters if improvement
1519                    if let Some(ref mut adaptive) = adaptive_state {
1520                        adaptive.record_success(f, cr);
1521                    }
1522
1523                    // Track if this is an improvement over the current best
1524                    if *trial_energy < best_f {
1525                        improvement_count += 1;
1526                    }
1527                }
1528            }
1529            let t_select = t_select0.elapsed();
1530
1531            t_build_tot += t_build;
1532            t_eval_tot += t_eval;
1533            t_select_tot += t_select;
1534            let iter_dur = iter_start.elapsed();
1535            t_iter_tot += iter_dur;
1536
1537            if timing_enabled && (iter <= 5 || iter % 10 == 0) {
1538                eprintln!(
1539                    "TIMING iter {:4}: build={:.3} ms, eval={:.3} ms, select={:.3} ms, total={:.3} ms",
1540                    iter,
1541                    t_build.as_secs_f64() * 1e3,
1542                    t_eval.as_secs_f64() * 1e3,
1543                    t_select.as_secs_f64() * 1e3,
1544                    iter_dur.as_secs_f64() * 1e3,
1545                );
1546            }
1547
1548            // Update adaptive parameters after each generation
1549            if let Some(ref mut adaptive) = adaptive_state {
1550                adaptive.update(&self.config.adaptive, iter, self.config.maxiter);
1551            }
1552
1553            // Update best solution after generation
1554            let (new_best_idx, new_best_f) = argmin(&energies);
1555            if new_best_f < best_f {
1556                best_idx = new_best_idx;
1557                best_f = new_best_f;
1558                best_x = pop.row(best_idx).to_owned();
1559            }
1560
1561            // Convergence check
1562            let pop_mean = energies.mean().unwrap_or(0.0);
1563            let pop_std = energies.std(0.0);
1564            let convergence_threshold = self.config.atol + self.config.tol * pop_mean.abs();
1565
1566            if self.config.disp {
1567                eprintln!(
1568                    "DE iter {:4}  best_f={:.6e}  std={:.3e}  accepted={}/{}, improved={}",
1569                    iter, best_f, pop_std, accepted_trials, npop, improvement_count
1570                );
1571            }
1572
1573            // Callback
1574            if let Some(ref mut cb) = self.config.callback {
1575                let intermediate = DEIntermediate {
1576                    x: best_x.clone(),
1577                    fun: best_f,
1578                    convergence: pop_std,
1579                    iter,
1580                };
1581                match cb(&intermediate) {
1582                    CallbackAction::Stop => {
1583                        success = true;
1584                        message = "Optimization stopped by callback".to_string();
1585                        break;
1586                    }
1587                    CallbackAction::Continue => {}
1588                }
1589            }
1590
1591            if pop_std <= convergence_threshold {
1592                success = true;
1593                message = format!(
1594                    "Converged: std(pop_f)={:.3e} <= threshold={:.3e}",
1595                    pop_std, convergence_threshold
1596                );
1597                break;
1598            }
1599        }
1600
1601        if !success {
1602            message = format!("Maximum iterations reached: {}", self.config.maxiter);
1603        }
1604
1605        if self.config.disp {
1606            eprintln!("DE finished: {}", message);
1607        }
1608
1609        // Polish if configured
1610        let (final_x, final_f, polish_nfev) = if let Some(ref polish_cfg) = self.config.polish {
1611            if polish_cfg.enabled {
1612                self.polish(&best_x)
1613            } else {
1614                (best_x.clone(), best_f, 0)
1615            }
1616        } else {
1617            (best_x.clone(), best_f, 0)
1618        };
1619
1620        if timing_enabled {
1621            eprintln!(
1622                "TIMING total: build={:.3} s, eval={:.3} s, select={:.3} s, iter_total={:.3} s",
1623                t_build_tot.as_secs_f64(),
1624                t_eval_tot.as_secs_f64(),
1625                t_select_tot.as_secs_f64(),
1626                t_iter_tot.as_secs_f64()
1627            );
1628        }
1629
1630        self.finish_report(
1631            pop,
1632            energies,
1633            final_x,
1634            final_f,
1635            success,
1636            message,
1637            nit,
1638            nfev + polish_nfev,
1639        )
1640    }
1641}
1642
1643#[cfg(test)]
1644mod strategy_tests {
1645    use super::*;
1646
1647    #[test]
1648    fn test_parse_strategy_variants() {
1649        assert!(matches!(
1650            "best1exp".parse::<Strategy>().unwrap(),
1651            Strategy::Best1Exp
1652        ));
1653        assert!(matches!(
1654            "rand1bin".parse::<Strategy>().unwrap(),
1655            Strategy::Rand1Bin
1656        ));
1657        assert!(matches!(
1658            "randtobest1exp".parse::<Strategy>().unwrap(),
1659            Strategy::RandToBest1Exp
1660        ));
1661    }
1662}