use crate::error::OptimError;
use crate::types::{IterationRecord, OptimResult, OptimStatus};
use numra_core::Scalar;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
#[derive(Clone, Debug)]
pub struct DEOptions<S: Scalar> {
pub pop_size: Option<usize>,
pub max_generations: usize,
pub differential_weight: S,
pub crossover_rate: S,
pub tol: S,
pub seed: u64,
pub verbose: bool,
}
impl<S: Scalar> Default for DEOptions<S> {
fn default() -> Self {
Self {
pop_size: None,
max_generations: 1000,
differential_weight: S::from_f64(0.8),
crossover_rate: S::from_f64(0.9),
tol: S::from_f64(1e-12),
seed: 42,
verbose: false,
}
}
}
pub fn de_minimize<S: Scalar, F>(
f: F,
bounds: &[(S, S)],
opts: &DEOptions<S>,
) -> Result<OptimResult<S>, OptimError>
where
F: Fn(&[S]) -> S,
{
let start = std::time::Instant::now();
let n = bounds.len();
let np = opts.pop_size.unwrap_or(10 * n);
let cr = opts.crossover_rate;
let f_weight = opts.differential_weight;
let mut rng = SmallRng::seed_from_u64(opts.seed);
let mut pop: Vec<Vec<S>> = (0..np)
.map(|_| {
bounds
.iter()
.map(|&(lo, hi)| lo + S::from_f64(rng.gen::<f64>()) * (hi - lo))
.collect()
})
.collect();
let mut fitness: Vec<S> = pop.iter().map(|xi| f(xi)).collect();
let mut n_feval: usize = np;
let mut best_idx: usize = 0;
for i in 1..np {
if fitness[i] < fitness[best_idx] {
best_idx = i;
}
}
let mut best_x = pop[best_idx].clone();
let mut best_f = fitness[best_idx];
let mut history: Vec<IterationRecord<S>> = Vec::new();
let mut converged = false;
let mut gen = 0usize;
for g in 0..opts.max_generations {
gen = g + 1;
for i in 0..np {
let (a, b, c) = pick_three(&mut rng, np, i);
let mut trial = vec![S::ZERO; n];
let j_rand = rng.gen_range(0..n);
for j in 0..n {
let v_j = pop[a][j] + f_weight * (pop[b][j] - pop[c][j]);
let v_j = v_j.clamp(bounds[j].0, bounds[j].1);
if S::from_f64(rng.gen::<f64>()) < cr || j == j_rand {
trial[j] = v_j;
} else {
trial[j] = pop[i][j];
}
}
let trial_f = f(&trial);
n_feval += 1;
if trial_f <= fitness[i] {
pop[i] = trial;
fitness[i] = trial_f;
if trial_f < best_f {
best_f = trial_f;
best_x = pop[i].clone();
}
}
}
history.push(IterationRecord {
iteration: gen,
objective: best_f,
gradient_norm: S::ZERO,
step_size: S::ZERO,
constraint_violation: S::ZERO,
});
if opts.verbose {
eprintln!("DE gen {}: best_f = {:.6e}", gen, best_f.to_f64());
}
let f_max = fitness.iter().copied().fold(S::NEG_INFINITY, S::max);
let f_min = fitness.iter().copied().fold(S::INFINITY, S::min);
if (f_max - f_min) < opts.tol {
converged = true;
break;
}
}
let (status, message) = if converged {
(
OptimStatus::GradientConverged,
format!(
"Converged: fitness spread < tol ({:.2e}) at generation {}",
opts.tol.to_f64(),
gen
),
)
} else {
(
OptimStatus::MaxIterations,
format!(
"Reached maximum generations ({}), best f = {:.6e}",
opts.max_generations,
best_f.to_f64()
),
)
};
let result = OptimResult {
x: best_x,
f: best_f,
grad: Vec::new(),
iterations: gen,
n_feval,
n_geval: 0,
converged,
message,
status,
history,
lambda_eq: Vec::new(),
lambda_ineq: Vec::new(),
active_bounds: Vec::new(),
constraint_violation: S::ZERO,
wall_time_secs: 0.0,
pareto: None,
sensitivity: None,
}
.with_wall_time(start);
Ok(result)
}
fn pick_three(rng: &mut SmallRng, n: usize, exclude: usize) -> (usize, usize, usize) {
let mut a = rng.gen_range(0..n);
while a == exclude {
a = rng.gen_range(0..n);
}
let mut b = rng.gen_range(0..n);
while b == exclude || b == a {
b = rng.gen_range(0..n);
}
let mut c = rng.gen_range(0..n);
while c == exclude || c == a || c == b {
c = rng.gen_range(0..n);
}
(a, b, c)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_de_sphere() {
let bounds = vec![(-5.0, 5.0); 3];
let result = de_minimize(
|x: &[f64]| x.iter().map(|xi| xi * xi).sum::<f64>(),
&bounds,
&DEOptions {
max_generations: 500,
..Default::default()
},
)
.unwrap();
assert!(result.converged, "did not converge: {}", result.message);
for &xi in &result.x {
assert!(xi.abs() < 0.1, "xi={}", xi);
}
assert!(result.f < 0.01, "f={}", result.f);
}
#[test]
fn test_de_rastrigin() {
let bounds = vec![(-5.12, 5.12); 2];
let result = de_minimize(
|x: &[f64]| {
let n = x.len() as f64;
10.0 * n
+ x.iter()
.map(|xi| xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos())
.sum::<f64>()
},
&bounds,
&DEOptions {
max_generations: 1000,
..Default::default()
},
)
.unwrap();
assert!(result.f < 1.0, "f={}", result.f);
}
#[test]
fn test_de_rosenbrock() {
let bounds = vec![(-5.0, 10.0), (-5.0, 10.0)];
let result = de_minimize(
|x: &[f64]| (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2),
&bounds,
&DEOptions {
max_generations: 2000,
..Default::default()
},
)
.unwrap();
assert!(result.f < 0.01, "f={}", result.f);
assert!((result.x[0] - 1.0).abs() < 0.1, "x0={}", result.x[0]);
}
#[test]
fn test_de_deterministic() {
let bounds = vec![(-5.0, 5.0); 2];
let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
let r1 = de_minimize(f, &bounds, &DEOptions::default()).unwrap();
let r2 = de_minimize(f, &bounds, &DEOptions::default()).unwrap();
assert_eq!(r1.x, r2.x);
assert_eq!(r1.f, r2.f);
}
#[test]
fn test_de_1d() {
let bounds = vec![(0.0, 10.0)];
let result = de_minimize(
|x: &[f64]| (x[0] - 3.0).powi(2),
&bounds,
&DEOptions::default(),
)
.unwrap();
assert!((result.x[0] - 3.0).abs() < 0.1, "x={}", result.x[0]);
}
}