use crate::error::{NumRs2Error, Result};
use crate::optimize::OptimizeResult;
use num_traits::Float;
use scirs2_core::random::{thread_rng, Distribution, Rng, RngExt, Uniform};
use std::cmp::Ordering;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MutationStrategy {
Rand1,
Best1,
Rand2,
CurrentToBest1,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CrossoverType {
Binomial,
Exponential,
}
#[derive(Debug, Clone)]
pub struct DEConfig<T: Float> {
pub pop_size: usize,
pub max_generations: usize,
pub mutation_factor: T,
pub crossover_rate: T,
pub strategy: MutationStrategy,
pub crossover: CrossoverType,
pub adaptive: bool,
pub ftol: T,
}
impl<T: Float> DEConfig<T> {
pub fn new(dimension: usize) -> Self {
Self {
pop_size: (15 * dimension).max(50),
max_generations: 200,
mutation_factor: T::from(0.8).expect("0.8 should convert to Float"),
crossover_rate: T::from(0.9).expect("0.9 should convert to Float"),
strategy: MutationStrategy::Rand1,
crossover: CrossoverType::Binomial,
adaptive: false,
ftol: T::from(1e-8).expect("1e-8 should convert to Float"),
}
}
}
impl<T: Float> Default for DEConfig<T> {
fn default() -> Self {
Self::new(2)
}
}
#[derive(Clone)]
struct Individual<T: Float> {
vector: Vec<T>,
fitness: T,
mutation_factor: T,
crossover_rate: T,
}
pub fn differential_evolution<T, F>(
f: F,
bounds: &[(T, T)],
config: Option<DEConfig<T>>,
) -> Result<OptimizeResult<T>>
where
T: Float + std::fmt::Display + std::iter::Sum,
F: Fn(&[T]) -> T,
{
let dim = bounds.len();
let config = config.unwrap_or_else(|| DEConfig::new(dim));
if dim == 0 {
return Err(NumRs2Error::ValueError(
"Bounds must have at least one dimension".to_string(),
));
}
if config.pop_size < 4 {
return Err(NumRs2Error::ValueError(
"Population size must be at least 4 for DE".to_string(),
));
}
let mut rng = thread_rng();
let mut population = initialize_population::<T, F>(
&f,
bounds,
config.pop_size,
config.mutation_factor,
config.crossover_rate,
&mut rng,
)?;
population.sort_by(|a, b| a.fitness.partial_cmp(&b.fitness).unwrap_or(Ordering::Equal));
let mut best = population[0].clone();
let mut nfev = config.pop_size;
let njev = 0;
let mut stagnation_count = 0;
let max_stagnation = (dim * 3).clamp(30, 100);
for generation in 0..config.max_generations {
let prev_best_fitness = best.fitness;
let mut new_population = Vec::with_capacity(config.pop_size);
for i in 0..config.pop_size {
let target = &population[i];
let (f_mutation, cr) = if config.adaptive {
update_adaptive_parameters(target.mutation_factor, target.crossover_rate, &mut rng)?
} else {
(config.mutation_factor, config.crossover_rate)
};
let mutant = mutate(
&population,
i,
&config.strategy,
f_mutation,
bounds,
&mut rng,
)?;
let trial = crossover(&target.vector, &mutant, &config.crossover, cr, &mut rng)?;
let trial = enforce_bounds(&trial, bounds);
let trial_fitness = f(&trial);
nfev += 1;
if trial_fitness < target.fitness {
let trial_clone = if trial_fitness < best.fitness {
Some(trial.clone())
} else {
None
};
new_population.push(Individual {
vector: trial,
fitness: trial_fitness,
mutation_factor: f_mutation,
crossover_rate: cr,
});
if let Some(trial_vec) = trial_clone {
best = Individual {
vector: trial_vec,
fitness: trial_fitness,
mutation_factor: f_mutation,
crossover_rate: cr,
};
}
} else {
new_population.push(target.clone());
}
}
population = new_population;
let improvement = (prev_best_fitness - best.fitness).abs();
if improvement < config.ftol && generation > 10 {
return Ok(OptimizeResult {
x: best.vector,
fun: best.fitness,
grad: vec![T::zero(); dim],
nit: generation + 1,
nfev,
njev,
success: true,
message: "Convergence achieved".to_string(),
});
}
if improvement < config.ftol {
stagnation_count += 1;
} else {
stagnation_count = 0;
}
if stagnation_count >= max_stagnation {
return Ok(OptimizeResult {
x: best.vector,
fun: best.fitness,
grad: vec![T::zero(); dim],
nit: generation + 1,
nfev,
njev,
success: true,
message: "Stopped due to stagnation".to_string(),
});
}
}
Ok(OptimizeResult {
x: best.vector,
fun: best.fitness,
grad: vec![T::zero(); dim],
nit: config.max_generations,
nfev,
njev,
success: true,
message: "Maximum generations reached".to_string(),
})
}
fn initialize_population<T, F>(
f: &F,
bounds: &[(T, T)],
pop_size: usize,
mutation_factor: T,
crossover_rate: T,
rng: &mut impl Rng,
) -> Result<Vec<Individual<T>>>
where
T: Float + std::fmt::Display,
F: Fn(&[T]) -> T,
{
let dim = bounds.len();
let mut population = Vec::with_capacity(pop_size);
for _ in 0..pop_size {
let mut vector = Vec::with_capacity(dim);
for &(lower, upper) in bounds {
let lower_f64 = lower.to_f64().ok_or_else(|| {
NumRs2Error::ConversionError("Bound conversion failed".to_string())
})?;
let upper_f64 = upper.to_f64().ok_or_else(|| {
NumRs2Error::ConversionError("Bound conversion failed".to_string())
})?;
let uniform = Uniform::new(lower_f64, upper_f64).map_err(|e| {
NumRs2Error::ComputationError(format!("Uniform creation failed: {}", e))
})?;
let value = T::from(uniform.sample(rng)).ok_or_else(|| {
NumRs2Error::ConversionError("Sample conversion failed".to_string())
})?;
vector.push(value);
}
let fitness = f(&vector);
population.push(Individual {
vector,
fitness,
mutation_factor,
crossover_rate,
});
}
Ok(population)
}
fn mutate<T: Float>(
population: &[Individual<T>],
current_idx: usize,
strategy: &MutationStrategy,
f_mutation: T,
bounds: &[(T, T)],
rng: &mut impl Rng,
) -> Result<Vec<T>> {
let n = population.len();
let dim = population[0].vector.len();
let mut get_random_indices = |count: usize, exclude: usize| -> Result<Vec<usize>> {
let mut indices = Vec::new();
let mut attempts = 0;
let max_attempts = count * 100;
while indices.len() < count && attempts < max_attempts {
let idx = (rng.random::<f64>() * n as f64) as usize % n;
if idx != exclude && !indices.contains(&idx) {
indices.push(idx);
}
attempts += 1;
}
if indices.len() < count {
return Err(NumRs2Error::ComputationError(
"Failed to find distinct random indices".to_string(),
));
}
Ok(indices)
};
let mutant = match strategy {
MutationStrategy::Rand1 => {
let indices = get_random_indices(3, current_idx)?;
let (r1, r2, r3) = (indices[0], indices[1], indices[2]);
(0..dim)
.map(|j| {
population[r1].vector[j]
+ f_mutation * (population[r2].vector[j] - population[r3].vector[j])
})
.collect()
}
MutationStrategy::Best1 => {
let indices = get_random_indices(2, current_idx)?;
let (r1, r2) = (indices[0], indices[1]);
(0..dim)
.map(|j| {
population[0].vector[j]
+ f_mutation * (population[r1].vector[j] - population[r2].vector[j])
})
.collect()
}
MutationStrategy::Rand2 => {
let indices = get_random_indices(5, current_idx)?;
let (r1, r2, r3, r4, r5) = (indices[0], indices[1], indices[2], indices[3], indices[4]);
(0..dim)
.map(|j| {
population[r1].vector[j]
+ f_mutation * (population[r2].vector[j] - population[r3].vector[j])
+ f_mutation * (population[r4].vector[j] - population[r5].vector[j])
})
.collect()
}
MutationStrategy::CurrentToBest1 => {
let indices = get_random_indices(2, current_idx)?;
let (r1, r2) = (indices[0], indices[1]);
(0..dim)
.map(|j| {
population[current_idx].vector[j]
+ f_mutation * (population[0].vector[j] - population[current_idx].vector[j])
+ f_mutation * (population[r1].vector[j] - population[r2].vector[j])
})
.collect()
}
};
Ok(mutant)
}
fn crossover<T: Float>(
target: &[T],
mutant: &[T],
crossover_type: &CrossoverType,
cr: T,
rng: &mut impl Rng,
) -> Result<Vec<T>> {
let dim = target.len();
let mut trial = Vec::with_capacity(dim);
match crossover_type {
CrossoverType::Binomial => {
let j_rand = (rng.random::<f64>() * dim as f64) as usize % dim;
for j in 0..dim {
let rand_val = T::from(rng.random::<f64>()).ok_or_else(|| {
NumRs2Error::ConversionError("Random value conversion failed".to_string())
})?;
if rand_val < cr || j == j_rand {
trial.push(mutant[j]);
} else {
trial.push(target[j]);
}
}
}
CrossoverType::Exponential => {
let j_start = (rng.random::<f64>() * dim as f64) as usize % dim;
let mut j = j_start;
let mut l = 0;
loop {
trial.push(mutant[j]);
j = (j + 1) % dim;
l += 1;
let rand_val = T::from(rng.random::<f64>()).ok_or_else(|| {
NumRs2Error::ConversionError("Random value conversion failed".to_string())
})?;
if rand_val >= cr || l >= dim {
break;
}
}
while trial.len() < dim {
trial.push(target[j]);
j = (j + 1) % dim;
}
}
}
Ok(trial)
}
fn enforce_bounds<T: Float>(vector: &[T], bounds: &[(T, T)]) -> Vec<T> {
vector
.iter()
.zip(bounds.iter())
.map(|(&val, &(lower, upper))| val.max(lower).min(upper))
.collect()
}
fn update_adaptive_parameters<T: Float>(
current_f: T,
current_cr: T,
rng: &mut impl Rng,
) -> Result<(T, T)> {
let tau1 = 0.1;
let tau2 = 0.1;
let f_lower = 0.1;
let f_upper = 0.9;
let rand1 = rng.random::<f64>();
let rand2 = rng.random::<f64>();
let rand3 = rng.random::<f64>();
let rand4 = rng.random::<f64>();
let new_f = if rand2 < tau1 {
T::from(f_lower + rand1 * f_upper).ok_or_else(|| {
NumRs2Error::ConversionError("Mutation factor conversion failed".to_string())
})?
} else {
current_f
};
let new_cr = if rand4 < tau2 {
T::from(rand3).ok_or_else(|| {
NumRs2Error::ConversionError("Crossover rate conversion failed".to_string())
})?
} else {
current_cr
};
Ok((new_f, new_cr))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_de_sphere_function() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let config = DEConfig {
strategy: MutationStrategy::Best1,
pop_size: 180,
max_generations: 400,
mutation_factor: 0.85,
crossover_rate: 0.9,
ftol: 1e-10,
..DEConfig::new(2)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
assert!(result.fun < 0.01, "Should find good solution");
assert_relative_eq!(result.x[0], 0.0, epsilon = 0.3);
assert_relative_eq!(result.x[1], 0.0, epsilon = 0.3);
}
#[test]
fn test_de_rosenbrock() {
let f = |x: &[f64]| {
let (x0, x1) = (x[0], x[1]);
(1.0 - x0).powi(2) + 100.0 * (x1 - x0 * x0).powi(2)
};
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let config = DEConfig {
pop_size: 60,
max_generations: 300,
..DEConfig::new(2)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
assert!(
result.fun < 5.0,
"Should find reasonable solution (got {})",
result.fun
);
}
#[test]
fn test_de_best1_strategy() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let config = DEConfig {
strategy: MutationStrategy::Best1,
pop_size: 40,
max_generations: 50,
..DEConfig::new(2)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
}
#[test]
fn test_de_rand2_strategy() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let config = DEConfig {
strategy: MutationStrategy::Rand2,
pop_size: 40,
max_generations: 50,
..DEConfig::new(2)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
}
#[test]
fn test_de_current_to_best1_strategy() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let config = DEConfig {
strategy: MutationStrategy::CurrentToBest1,
pop_size: 40,
max_generations: 50,
..DEConfig::new(2)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
}
#[test]
fn test_de_exponential_crossover() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let config = DEConfig {
crossover: CrossoverType::Exponential,
pop_size: 40,
max_generations: 50,
..DEConfig::new(2)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
}
#[test]
fn test_de_adaptive() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let config = DEConfig {
adaptive: true,
pop_size: 40,
max_generations: 50,
..DEConfig::new(2)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
}
#[test]
fn test_de_high_dimensional() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0); 20];
let config = DEConfig {
strategy: MutationStrategy::Best1,
pop_size: 1000,
max_generations: 1000,
mutation_factor: 0.8,
crossover_rate: 0.9,
ftol: 1.0,
..DEConfig::new(20)
};
let result = differential_evolution(f, &bounds, Some(config)).expect("DE should succeed");
assert!(result.success);
assert!(
result.fun < 90.0,
"High-dimensional sphere convergence failure: got f={}",
result.fun
);
}
#[test]
fn test_de_invalid_bounds() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds: Vec<(f64, f64)> = vec![];
let result = differential_evolution(f, &bounds, None);
assert!(result.is_err());
}
#[test]
fn test_de_invalid_pop_size() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum();
let bounds = vec![(-5.0, 5.0)];
let config = DEConfig {
pop_size: 3,
..DEConfig::new(1)
};
let result = differential_evolution(f, &bounds, Some(config));
assert!(result.is_err());
}
}