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