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