use crate::core::constraint::BoxConstraints;
use crate::core::math::{SampleUniformBox, ScaleInPlace, ScaledAdd, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::rng::{ChaCha8Rng, Rng, RngExt, SeedableRng};
use crate::core::solver::Solver;
use crate::core::state::BasicPopulationState;
use crate::core::termination::TerminationReason;
use crate::solver::cma_es::sort_population_ascending;
pub struct De {
pop_size_override: Option<usize>,
f: f64,
cr: f64,
seed: u64,
rng: Option<ChaCha8Rng>,
}
impl De {
pub fn new(seed: u64) -> Self {
Self {
pop_size_override: None,
f: 0.8,
cr: 0.9,
seed,
rng: None,
}
}
pub fn default_pop_size(n: usize) -> usize {
(10 * n).max(4)
}
pub fn with_pop_size(mut self, pop_size: usize) -> Self {
assert!(
pop_size >= 4,
"De requires pop_size >= 4 (DE/rand/1 mutation needs three peers distinct from the target), got {}",
pop_size
);
self.pop_size_override = Some(pop_size);
self
}
pub fn with_f(mut self, f: f64) -> Self {
assert!(
f.is_finite() && f > 0.0,
"De requires F > 0 and finite, got {}",
f
);
self.f = f;
self
}
pub fn with_cr(mut self, cr: f64) -> Self {
assert!(
(0.0..=1.0).contains(&cr),
"De requires CR in [0, 1], got {}",
cr
);
self.cr = cr;
self
}
}
pub(crate) fn pick_three_distinct<R>(n: usize, exclude: usize, rng: &mut R) -> (usize, usize, usize)
where
R: Rng + ?Sized,
{
debug_assert!(n >= 4, "pick_three_distinct needs n >= 4 (got n = {})", n);
let r1 = loop {
let k = rng.random_range(0..n);
if k != exclude {
break k;
}
};
let r2 = loop {
let k = rng.random_range(0..n);
if k != exclude && k != r1 {
break k;
}
};
let r3 = loop {
let k = rng.random_range(0..n);
if k != exclude && k != r1 && k != r2 {
break k;
}
};
(r1, r2, r3)
}
pub(crate) fn de_rand_1_mutate<V>(x_r1: &V, x_r2: &V, x_r3: &V, f: f64) -> V
where
V: Clone + ScaledAdd<f64> + ScaleInPlace,
{
let mut v = x_r2.clone();
v.scaled_add(-1.0, x_r3);
v.scale_in_place(f);
v.scaled_add(1.0, x_r1);
v
}
pub(crate) fn repair_reinit_per_coord<V, R>(v: &mut V, lower: &V, upper: &V, rng: &mut R)
where
V: VectorLen + std::ops::Index<usize, Output = f64> + std::ops::IndexMut<usize, Output = f64>,
R: Rng + ?Sized,
{
let n = v.vec_len();
for j in 0..n {
if v[j] < lower[j] || v[j] > upper[j] {
v[j] = rng.random_range(lower[j]..=upper[j]);
}
}
}
pub(crate) fn binomial_crossover<V, R>(target: &V, donor: &V, cr: f64, rng: &mut R) -> V
where
V: VectorLen
+ Clone
+ std::ops::Index<usize, Output = f64>
+ std::ops::IndexMut<usize, Output = f64>,
R: Rng + ?Sized,
{
let n = target.vec_len();
let j_rand = rng.random_range(0..n);
let mut u = target.clone();
for j in 0..n {
if j == j_rand || rng.random::<f64>() < cr {
u[j] = donor[j];
}
}
u
}
impl<P, V> Solver<P, BasicPopulationState<V>> for De
where
P: CostFunction<Param = V, Output = f64> + BoxConstraints<Param = V>,
V: VectorLen
+ Clone
+ SampleUniformBox
+ ScaledAdd<f64>
+ ScaleInPlace
+ std::ops::Index<usize, Output = f64>
+ std::ops::IndexMut<usize, Output = f64>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: BasicPopulationState<V>,
) -> Result<BasicPopulationState<V>, Self::Error> {
let lo = problem.inner().lower().clone();
let hi = problem.inner().upper().clone();
let n = lo.vec_len();
let pop_size = self
.pop_size_override
.unwrap_or_else(|| Self::default_pop_size(n));
assert!(pop_size >= 4, "De requires pop_size >= 4 (got {pop_size})");
let mut rng = ChaCha8Rng::seed_from_u64(self.seed);
state.candidates.clear();
state.costs.clear();
for _ in 0..pop_size {
let x = V::sample_uniform_box(&lo, &hi, &mut rng);
let c = problem.cost(&x)?;
state.candidates.push(x);
state.costs.push(c);
}
sort_population_ascending(&mut state.candidates, &mut state.costs);
self.rng = Some(rng);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: BasicPopulationState<V>,
) -> Result<(BasicPopulationState<V>, Option<TerminationReason>), Self::Error> {
let lo = problem.inner().lower().clone();
let hi = problem.inner().upper().clone();
let rng = self
.rng
.as_mut()
.expect("De::init must run before next_iter");
let np = state.candidates.len();
let mut trials: Vec<V> = Vec::with_capacity(np);
let mut trial_costs: Vec<f64> = Vec::with_capacity(np);
for i in 0..np {
let (r1, r2, r3) = pick_three_distinct(np, i, rng);
let mut donor = de_rand_1_mutate(
&state.candidates[r1],
&state.candidates[r2],
&state.candidates[r3],
self.f,
);
repair_reinit_per_coord(&mut donor, &lo, &hi, rng);
let trial = binomial_crossover(&state.candidates[i], &donor, self.cr, rng);
let c_trial = problem.cost(&trial)?;
trials.push(trial);
trial_costs.push(c_trial);
}
for (i, (trial, c_trial)) in trials.into_iter().zip(trial_costs).enumerate() {
if c_trial <= state.costs[i] {
state.candidates[i] = trial;
state.costs[i] = c_trial;
}
}
sort_population_ascending(&mut state.candidates, &mut state.costs);
Ok((state, None))
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand_chacha::ChaCha8Rng;
#[test]
fn default_pop_size_uses_ten_d_with_floor_of_four() {
assert_eq!(De::default_pop_size(0), 4);
assert_eq!(De::default_pop_size(1), 10);
assert_eq!(De::default_pop_size(5), 50);
assert_eq!(De::default_pop_size(20), 200);
}
#[test]
fn pick_three_distinct_returns_pairwise_distinct_indices_avoiding_target() {
let mut rng = ChaCha8Rng::seed_from_u64(42);
for _ in 0..2_000 {
let (r1, r2, r3) = pick_three_distinct(10, 4, &mut rng);
assert_ne!(r1, 4);
assert_ne!(r2, 4);
assert_ne!(r3, 4);
assert_ne!(r1, r2);
assert_ne!(r1, r3);
assert_ne!(r2, r3);
assert!(r1 < 10 && r2 < 10 && r3 < 10);
}
}
#[test]
fn de_rand_1_mutate_computes_x_r1_plus_f_times_diff() {
let x_r1 = vec![1.0, 2.0];
let x_r2 = vec![4.0, 5.0];
let x_r3 = vec![3.0, 3.0];
let v = de_rand_1_mutate(&x_r1, &x_r2, &x_r3, 0.5);
assert!((v[0] - 1.5).abs() < 1e-12, "v[0] = {}", v[0]);
assert!((v[1] - 3.0).abs() < 1e-12, "v[1] = {}", v[1]);
}
#[test]
fn repair_reinit_per_coord_only_touches_violated_coordinates() {
let mut rng = ChaCha8Rng::seed_from_u64(7);
let lo = vec![0.0, 0.0, 0.0];
let hi = vec![1.0, 1.0, 1.0];
let mut v = vec![0.5, -2.0, 3.0]; repair_reinit_per_coord(&mut v, &lo, &hi, &mut rng);
assert_eq!(v[0], 0.5, "in-box coord must be untouched");
assert!(
(0.0..=1.0).contains(&v[1]),
"below-bound coord must land in [0, 1], got {}",
v[1]
);
assert!(
(0.0..=1.0).contains(&v[2]),
"above-bound coord must land in [0, 1], got {}",
v[2]
);
}
#[test]
fn binomial_crossover_with_cr_zero_takes_exactly_one_donor_coordinate() {
let target = vec![0.0, 0.0, 0.0, 0.0, 0.0];
let donor = vec![1.0, 1.0, 1.0, 1.0, 1.0];
for seed in 0..50 {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let u = binomial_crossover(&target, &donor, 0.0, &mut rng);
let donor_count = u.iter().filter(|&&x| x == 1.0).count();
assert_eq!(
donor_count, 1,
"with CR = 0, exactly one donor coordinate expected; got u = {:?}",
u
);
}
}
#[test]
fn binomial_crossover_with_cr_one_takes_all_donor_coordinates() {
let target = vec![0.0, 0.0, 0.0];
let donor = vec![1.0, 1.0, 1.0];
let mut rng = ChaCha8Rng::seed_from_u64(123);
let u = binomial_crossover(&target, &donor, 1.0, &mut rng);
assert_eq!(u, donor);
}
}