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