use rand_distr::uniform::SampleUniform;
use crate::core::constraint::evaluation::BoxProblem;
use crate::core::math::{
SampleUniformBox, Scalar, 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::PopulationProgress;
use crate::solver::cma_es::sort_population_ascending;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeMutation {
#[default]
Rand1,
Best1,
Rand2,
Best2,
RandToBest1,
CurrentToBest1,
}
impl DeMutation {
fn peer_count(self) -> usize {
match self {
Self::Best1 | Self::CurrentToBest1 => 2,
Self::Rand1 | Self::RandToBest1 => 3,
Self::Best2 => 4,
Self::Rand2 => 5,
}
}
fn minimum_pop_size(self) -> usize {
(self.peer_count() + 1).max(4)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeCrossover {
#[default]
Binomial,
Exponential,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct De<F: Scalar = f64> {
pop_size_override: Option<usize>,
f: F,
cr: f64,
seed: u64,
rng: Option<ChaCha8Rng>,
mutation: DeMutation,
crossover: DeCrossover,
dither: Option<(F, F)>,
}
impl<F: Scalar> De<F> {
pub fn new(seed: u64) -> Self {
Self {
pop_size_override: None,
f: F::from_f64(0.8).unwrap(),
cr: 0.9,
seed,
rng: None,
mutation: DeMutation::Rand1,
crossover: DeCrossover::Binomial,
dither: None,
}
}
}
impl<F: Scalar> De<F> {
pub fn default_pop_size(n: usize) -> usize {
(10 * n).max(4)
}
}
impl<F: Scalar> De<F> {
pub fn with_mutation(mut self, mutation: DeMutation) -> Self {
self.mutation = mutation;
self
}
pub fn with_crossover(mut self, crossover: DeCrossover) -> Self {
self.crossover = crossover;
self
}
pub fn with_dither(mut self, min: F, max: F) -> Self {
assert!(
min.is_finite() && max.is_finite() && min > F::zero() && min < max,
"De dithering requires finite 0 < min < max, got {min:?}, {max:?}"
);
self.dither = Some((min, max));
self
}
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: F) -> Self {
assert!(
f.is_finite() && f > F::zero(),
"De requires F > 0 and finite, got {:?}",
f
);
self.f = f;
self.dither = None;
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
}
}
fn pick_distinct<R>(
n: usize,
exclude: usize,
count: usize,
rng: &mut R,
) -> [usize; 5]
where
R: Rng + ?Sized,
{
debug_assert!(count <= 5 && n > count && exclude < n);
let mut peers = [0; 5];
for j in 0..count {
peers[j] = loop {
let k = rng.random_range(0..n);
if k != exclude && !peers[..j].contains(&k) {
break k;
}
};
}
peers
}
pub(crate) fn de_rand_1_mutate<V, F>(x_r1: &V, x_r2: &V, x_r3: &V, f: F) -> V
where
F: Scalar,
V: Clone + ScaledAdd<F> + ScaleInPlace<F>,
{
let mut v = x_r2.clone();
v.scaled_add(-F::one(), x_r3);
v.scale_in_place(f);
v.scaled_add(F::one(), x_r1);
v
}
fn mutate<V, F>(
mutation: DeMutation,
population: &[V],
target: usize,
peers: &[usize],
f: F,
) -> V
where
F: Scalar,
V: Clone + ScaledAdd<F> + ScaleInPlace<F>,
{
let best = &population[0];
match mutation {
DeMutation::Rand1 => de_rand_1_mutate(
&population[peers[0]],
&population[peers[1]],
&population[peers[2]],
f,
),
DeMutation::Best1 => de_rand_1_mutate(
best,
&population[peers[0]],
&population[peers[1]],
f,
),
DeMutation::Rand2 | DeMutation::Best2 => {
let (base, differences) = if mutation == DeMutation::Rand2 {
(&population[peers[0]], &peers[1..])
} else {
(best, peers)
};
let mut donor = population[differences[0]].clone();
donor.scaled_add(F::one(), &population[differences[1]]);
donor.scaled_add(-F::one(), &population[differences[2]]);
donor.scaled_add(-F::one(), &population[differences[3]]);
donor.scale_in_place(f);
donor.scaled_add(F::one(), base);
donor
}
DeMutation::RandToBest1 => {
let base = &population[peers[0]];
let toward_best = de_rand_1_mutate(base, best, base, f);
de_rand_1_mutate(
&toward_best,
&population[peers[1]],
&population[peers[2]],
f,
)
}
DeMutation::CurrentToBest1 => {
let base = &population[target];
let mut donor = best.clone();
donor.scaled_add(-F::one(), base);
donor.scaled_add(F::one(), &population[peers[0]]);
donor.scaled_add(-F::one(), &population[peers[1]]);
donor.scale_in_place(f);
donor.scaled_add(F::one(), base);
donor
}
}
}
pub(crate) fn repair_reinit_per_coord<V, F, R>(
v: &mut V,
lower: &V,
upper: &V,
rng: &mut R,
) where
F: Scalar + SampleUniform,
V: VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
R: Rng + ?Sized,
{
let n = v.vec_len();
for j in 0..n {
if !v[j].is_finite() || v[j] < lower[j] || v[j] > upper[j] {
v[j] = rng.random_range(lower[j]..=upper[j]);
}
}
}
pub(crate) fn binomial_crossover<V, F, R>(
target: &V,
donor: &V,
cr: f64,
rng: &mut R,
) -> V
where
F: Scalar,
V: VectorLen
+ Clone
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
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
}
fn exponential_crossover<V, F, R>(
target: &V,
donor: &V,
cr: f64,
rng: &mut R,
) -> V
where
F: Scalar,
V: VectorLen
+ Clone
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
R: Rng + ?Sized,
{
let n = target.vec_len();
let start = rng.random_range(0..n);
let mut trial = target.clone();
trial[start] = donor[start];
for offset in 1..n {
if rng.random::<f64>() >= cr {
break;
}
let j = (start + offset) % n;
trial[j] = donor[j];
}
trial
}
impl<P, V, F> Solver<P, PopulationProgress<V, F>> for De<F>
where
F: Scalar + SampleUniform + crate::core::parallel::MaybeSend,
P: CostFunction<Param = V, Output = F>
+ BoxProblem<Param = V>
+ crate::core::parallel::MaybeSync,
P::Error: crate::core::parallel::MaybeSend,
V: VectorLen
+ Clone
+ SampleUniformBox
+ ScaledAdd<F>
+ ScaleInPlace<F>
+ crate::core::parallel::MaybeSync
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
V: crate::VectorLen + std::ops::Index<usize, Output = F>,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PopulationProgress<V, F>,
) -> Result<PopulationProgress<V, F>, Self::Error> {
crate::core::constraint::evaluation::validate_box_problem(
problem.inner(),
None,
true,
)
.map_err(crate::core::constraint::evaluation::invalid)?;
let lo = problem.inner().lower().clone();
let hi = problem.inner().upper().clone();
let n = lo.vec_len();
assert!(n > 0, "De requires non-empty parameter bounds");
let pop_size = self
.pop_size_override
.unwrap_or_else(|| Self::default_pop_size(n));
let minimum = self.mutation.minimum_pop_size();
assert!(
pop_size >= minimum,
"De {:?} requires pop_size >= {minimum} (got {pop_size})",
self.mutation
);
let mut rng = ChaCha8Rng::seed_from_u64(self.seed);
state.reset();
super::population::prepare_population(
&mut state.candidates,
&lo,
&hi,
pop_size,
&mut rng,
);
state.costs = problem.cost_batch(&state.candidates)?;
sort_population_ascending(&mut state.candidates, &mut state.costs);
state.select_best_member();
self.rng = Some(rng);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PopulationProgress<V, F>,
) -> Result<crate::SolverStep<PopulationProgress<V, F>>, 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 f = match self.dither {
Some((min, max)) => rng.random_range(min..max),
None => self.f,
};
let mut trials: Vec<V> = Vec::with_capacity(np);
for i in 0..np {
let count = self.mutation.peer_count();
let peers = pick_distinct(np, i, count, rng);
let mut donor =
mutate(self.mutation, &state.candidates, i, &peers[..count], f);
repair_reinit_per_coord(&mut donor, &lo, &hi, rng);
let trial = match self.crossover {
DeCrossover::Binomial => binomial_crossover(
&state.candidates[i],
&donor,
self.cr,
rng,
),
DeCrossover::Exponential => exponential_crossover(
&state.candidates[i],
&donor,
self.cr,
rng,
),
};
trials.push(trial);
}
let trial_costs = problem.cost_batch(&trials)?;
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);
state.select_best_member();
Ok(crate::SolverStep::from((state, None)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand_chacha::ChaCha8Rng;
const MUTATIONS: [DeMutation; 6] = [
DeMutation::Rand1,
DeMutation::Best1,
DeMutation::Rand2,
DeMutation::Best2,
DeMutation::RandToBest1,
DeMutation::CurrentToBest1,
];
#[test]
fn mutation_donors_match_scipy_1_16_2() {
let population = vec![
vec![1.0, 2.0, 3.0],
vec![2.0, -1.0, 4.0],
vec![4.0, 5.0, -2.0],
vec![-3.0, 1.0, 2.0],
vec![6.0, -4.0, 1.0],
vec![0.0, 3.0, -5.0],
];
let rows: Vec<_> =
include_str!("../../tests/fixtures/de_mutations.tsv")
.lines()
.filter(|line| !line.starts_with('#'))
.collect();
assert_eq!(rows.len(), MUTATIONS.len());
for (mutation, row) in MUTATIONS.into_iter().zip(rows) {
let expected: Vec<f64> = row
.split_whitespace()
.skip(1)
.map(|x| x.parse().unwrap())
.collect();
let actual =
mutate(mutation, &population, 3, &[1, 2, 4, 5, 0], 0.5);
assert_eq!(actual, expected, "{mutation:?}");
}
}
#[test]
fn peer_sampling_excludes_target_at_minimum_population_sizes() {
let mut rng = ChaCha8Rng::seed_from_u64(123);
for count in 2..=5 {
let n = count + 1;
for target in 0..n {
for _ in 0..100 {
let peers = pick_distinct(n, target, count, &mut rng);
let mut actual = peers[..count].to_vec();
actual.sort_unstable();
let expected: Vec<_> =
(0..n).filter(|&i| i != target).collect();
assert_eq!(actual, expected);
}
}
}
}
#[test]
fn exponential_crossover_copies_one_wrapping_segment() {
let target = vec![0.0; 5];
let donor = vec![1.0; 5];
let mut saw_wrapping = false;
for seed in 0..100 {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let trial = exponential_crossover(&target, &donor, 0.6, &mut rng);
assert!(trial.contains(&1.0));
let transitions =
(0..5).filter(|&i| trial[i] != trial[(i + 1) % 5]).count();
assert!(transitions <= 2, "non-contiguous crossover: {trial:?}");
saw_wrapping |=
trial[0] == 1.0 && trial[4] == 1.0 && trial.contains(&0.0);
let trial = exponential_crossover(&target, &donor, 0.0, &mut rng);
assert_eq!(trial.iter().filter(|&&x| x == 1.0).count(), 1);
assert_eq!(
exponential_crossover(&target, &donor, 1.0, &mut rng),
donor
);
for cr in [0.0, 0.5, 1.0] {
assert_eq!(
exponential_crossover(&vec![0.0], &vec![1.0], cr, &mut rng),
vec![1.0]
);
}
}
assert!(saw_wrapping);
}
struct Plateau {
lower: Vec<f64>,
upper: Vec<f64>,
}
impl crate::ProblemConstraints for Plateau {
type Constraints = crate::Constraints<crate::BoxBounds<Vec<f64>>>;
fn constraint_set(
&self,
) -> <Self::Constraints as crate::ConstraintDescription>::Borrowed<'_>
{
crate::Constraints::new().with_bounds(crate::BoxBounds::new(
Some(&self.lower),
Some(&self.upper),
))
}
}
impl CostFunction for Plateau {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, _: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(0.0)
}
}
#[test]
fn dithering_draws_one_shared_scale_before_each_generation() {
for mutation in MUTATIONS {
for crossover in [DeCrossover::Binomial, DeCrossover::Exponential] {
let mut problem = Problem::new(Plateau {
lower: vec![-2.0; 3],
upper: vec![3.0; 3],
});
let make = || {
De::new(17)
.with_pop_size(8)
.with_mutation(mutation)
.with_crossover(crossover)
};
let mut dithered = make().with_dither(0.3, 1.7);
let mut fixed = make();
let mut actual = dithered
.init(&mut problem, PopulationProgress::empty())
.unwrap();
let mut expected = fixed
.init(&mut problem, PopulationProgress::empty())
.unwrap();
let mut scales = Vec::new();
for _ in 0..4 {
let mut rng = dithered.rng.clone().unwrap();
let scale = rng.random_range(0.3..1.7);
scales.push(scale);
fixed = fixed.with_f(scale);
fixed.rng = Some(rng);
actual =
dithered.next_iter(&mut problem, actual).unwrap().state;
expected =
fixed.next_iter(&mut problem, expected).unwrap().state;
assert_eq!(
actual.candidates, expected.candidates,
"{mutation:?} {crossover:?}"
);
assert_eq!(
dithered.rng.as_ref().unwrap().get_word_pos(),
fixed.rng.as_ref().unwrap().get_word_pos()
);
}
assert!(scales.windows(2).any(|pair| pair[0] != pair[1]));
}
}
}
#[test]
fn repair_reinitializes_non_finite_donor_coordinates() {
let mut rng = ChaCha8Rng::seed_from_u64(7);
let lo = vec![-1.0; 4];
let hi = vec![1.0; 4];
let mut donor = vec![f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 0.5];
repair_reinit_per_coord(&mut donor, &lo, &hi, &mut rng);
assert!(
donor
.iter()
.all(|x| x.is_finite() && (-1.0..=1.0).contains(x))
);
assert_eq!(donor[3], 0.5);
}
#[test]
fn default_pop_size_uses_ten_d_with_floor_of_four() {
assert_eq!(De::<f64>::default_pop_size(0), 4);
assert_eq!(De::<f64>::default_pop_size(1), 10);
assert_eq!(De::<f64>::default_pop_size(5), 50);
assert_eq!(De::<f64>::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_distinct(10, 4, 3, &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<f64> = vec![1.0, 2.0];
let x_r2: Vec<f64> = vec![4.0, 5.0];
let x_r3: Vec<f64> = 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);
}
}