use crate::error::{OptimizeError, OptimizeResult};
use crate::result::OptimizeResults;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{rng, Rng, SeedableRng};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DeStrategy {
Rand1,
Best1,
RandToBest1,
}
impl Default for DeStrategy {
fn default() -> Self {
DeStrategy::Rand1
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CrossoverType {
Binomial,
Exponential,
}
impl Default for CrossoverType {
fn default() -> Self {
CrossoverType::Binomial
}
}
#[derive(Debug, Clone)]
pub struct DeConstraintHandler {
pub penalty_coeff: f64,
pub use_feasibility_rules: bool,
}
impl Default for DeConstraintHandler {
fn default() -> Self {
Self {
penalty_coeff: 1e6,
use_feasibility_rules: true,
}
}
}
#[derive(Debug, Clone)]
pub struct OppositionBasedInit {
pub enabled: bool,
pub jumping_rate: f64,
}
impl Default for OppositionBasedInit {
fn default() -> Self {
Self {
enabled: true,
jumping_rate: 0.3,
}
}
}
#[derive(Debug, Clone)]
pub struct DeOptions {
pub pop_size: usize,
pub max_generations: usize,
pub mutation_factor: f64,
pub crossover_prob: f64,
pub strategy: DeStrategy,
pub crossover: CrossoverType,
pub bounds: Vec<(f64, f64)>,
pub seed: Option<u64>,
pub tol: f64,
pub patience: usize,
pub opposition: OppositionBasedInit,
pub constraint_handler: Option<DeConstraintHandler>,
}
impl Default for DeOptions {
fn default() -> Self {
Self {
pop_size: 50,
max_generations: 1000,
mutation_factor: 0.8,
crossover_prob: 0.9,
strategy: DeStrategy::Rand1,
crossover: CrossoverType::Binomial,
bounds: Vec::new(),
seed: None,
tol: 1e-12,
patience: 100,
opposition: OppositionBasedInit::default(),
constraint_handler: None,
}
}
}
#[derive(Debug, Clone)]
pub struct JdeOptions {
pub base: DeOptions,
pub tau_f: f64,
pub tau_cr: f64,
pub f_lower: f64,
pub f_upper: f64,
}
impl Default for JdeOptions {
fn default() -> Self {
Self {
base: DeOptions::default(),
tau_f: 0.1,
tau_cr: 0.1,
f_lower: 0.1,
f_upper: 0.9,
}
}
}
#[derive(Debug, Clone)]
pub struct DeResult {
pub x: Array1<f64>,
pub fun: f64,
pub nfev: usize,
pub generations: usize,
pub converged: bool,
pub message: String,
pub population_spread: f64,
}
impl DeResult {
pub fn to_optimize_results(&self) -> OptimizeResults<f64> {
OptimizeResults {
x: self.x.clone(),
fun: self.fun,
jac: None,
hess: None,
constr: None,
nit: self.generations,
nfev: self.nfev,
njev: 0,
nhev: 0,
maxcv: 0,
message: self.message.clone(),
success: self.converged,
status: if self.converged { 0 } else { 1 },
}
}
}
pub struct DifferentialEvolutionOptimizer {
options: DeOptions,
rng: StdRng,
}
impl DifferentialEvolutionOptimizer {
pub fn new(options: DeOptions) -> OptimizeResult<Self> {
if options.bounds.is_empty() {
return Err(OptimizeError::InvalidInput(
"Bounds must be provided for DE".to_string(),
));
}
if options.pop_size < 4 {
return Err(OptimizeError::InvalidParameter(
"Population size must be >= 4 for DE".to_string(),
));
}
if options.mutation_factor < 0.0 || options.mutation_factor > 2.0 {
return Err(OptimizeError::InvalidParameter(
"Mutation factor F must be in [0, 2]".to_string(),
));
}
if options.crossover_prob < 0.0 || options.crossover_prob > 1.0 {
return Err(OptimizeError::InvalidParameter(
"Crossover probability CR must be in [0, 1]".to_string(),
));
}
let seed = options.seed.unwrap_or_else(|| rng().random());
Ok(Self {
options,
rng: StdRng::seed_from_u64(seed),
})
}
pub fn optimize<F>(&mut self, func: F) -> OptimizeResult<DeResult>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
self.optimize_constrained(func, None::<fn(&ArrayView1<f64>) -> Vec<f64>>)
}
pub fn optimize_constrained<F, G>(
&mut self,
func: F,
constraints_fn: Option<G>,
) -> OptimizeResult<DeResult>
where
F: Fn(&ArrayView1<f64>) -> f64,
G: Fn(&ArrayView1<f64>) -> Vec<f64>,
{
let ndim = self.options.bounds.len();
let np = self.options.pop_size;
let mut population = self.initialize_population(ndim, np);
let mut fitness: Vec<f64> = Vec::with_capacity(np);
let mut violations: Vec<f64> = vec![0.0; np]; let mut nfev: usize = 0;
for i in 0..np {
let row = population.row(i);
let f_val = func(&row);
nfev += 1;
let viol = if let Some(ref cf) = constraints_fn {
let v = cf(&row);
v.iter().map(|vi| vi.max(0.0)).sum::<f64>()
} else {
0.0
};
fitness.push(self.penalized_fitness(f_val, viol));
violations[i] = viol;
}
let mut best_idx = self.find_best(&fitness, &violations);
let mut best_x = population.row(best_idx).to_owned();
let mut best_fun = func(&best_x.view());
let mut no_improve_count: usize = 0;
for gen in 0..self.options.max_generations {
let mut new_population = population.clone();
let mut new_fitness = fitness.clone();
let mut new_violations = violations.clone();
for i in 0..np {
let mutant = self.mutate(&population, i, best_idx, ndim);
let trial = self.crossover(&population.row(i).to_owned(), &mutant, ndim);
let trial_clipped = self.clip_to_bounds(&trial);
let trial_view = trial_clipped.view();
let trial_f = func(&trial_view);
nfev += 1;
let trial_viol = if let Some(ref cf) = constraints_fn {
let v = cf(&trial_view);
v.iter().map(|vi| vi.max(0.0)).sum::<f64>()
} else {
0.0
};
let trial_penalized = self.penalized_fitness(trial_f, trial_viol);
let replace = if self
.options
.constraint_handler
.as_ref()
.map_or(false, |ch| ch.use_feasibility_rules)
{
self.feasibility_selection(
trial_penalized,
trial_viol,
fitness[i],
violations[i],
)
} else {
trial_penalized <= fitness[i]
};
if replace {
for d in 0..ndim {
new_population[[i, d]] = trial_clipped[d];
}
new_fitness[i] = trial_penalized;
new_violations[i] = trial_viol;
}
}
population = new_population;
fitness = new_fitness;
violations = new_violations;
let new_best_idx = self.find_best(&fitness, &violations);
let candidate_fun = func(&population.row(new_best_idx));
if candidate_fun < best_fun {
best_idx = new_best_idx;
best_x = population.row(best_idx).to_owned();
best_fun = candidate_fun;
no_improve_count = 0;
} else {
best_idx = new_best_idx;
no_improve_count += 1;
}
let spread = self.population_spread(&fitness);
if spread < self.options.tol {
return Ok(DeResult {
x: best_x,
fun: best_fun,
nfev,
generations: gen + 1,
converged: true,
message: format!(
"DE converged: spread {:.2e} < tol {:.2e} at generation {}",
spread,
self.options.tol,
gen + 1
),
population_spread: spread,
});
}
if no_improve_count >= self.options.patience {
return Ok(DeResult {
x: best_x,
fun: best_fun,
nfev,
generations: gen + 1,
converged: true,
message: format!(
"DE converged: no improvement for {} generations",
self.options.patience
),
population_spread: spread,
});
}
}
let spread = self.population_spread(&fitness);
Ok(DeResult {
x: best_x,
fun: best_fun,
nfev,
generations: self.options.max_generations,
converged: false,
message: format!(
"DE completed {} generations without full convergence",
self.options.max_generations
),
population_spread: spread,
})
}
fn initialize_population(&mut self, ndim: usize, np: usize) -> Array2<f64> {
let mut pop = Array2::zeros((np, ndim));
for i in 0..np {
for d in 0..ndim {
let (lo, hi) = self.options.bounds[d];
pop[[i, d]] = lo + self.rng.random::<f64>() * (hi - lo);
}
}
if self.options.opposition.enabled {
let mut all_candidates = Vec::with_capacity(2 * np);
for i in 0..np {
let mut member = Vec::with_capacity(ndim);
let mut opposite = Vec::with_capacity(ndim);
for d in 0..ndim {
let val = pop[[i, d]];
member.push(val);
let (lo, hi) = self.options.bounds[d];
opposite.push(lo + hi - val);
}
all_candidates.push(member);
all_candidates.push(opposite);
}
for i in 0..np {
if i < all_candidates.len() / 2 {
let opp_idx = 2 * i + 1;
if opp_idx < all_candidates.len() && i % 2 == 1 {
for d in 0..ndim {
pop[[i, d]] = all_candidates[opp_idx][d];
}
}
}
}
}
pop
}
fn mutate(
&mut self,
population: &Array2<f64>,
target_idx: usize,
best_idx: usize,
ndim: usize,
) -> Array1<f64> {
let np = population.nrows();
let f = self.options.mutation_factor;
match self.options.strategy {
DeStrategy::Rand1 => {
let (r1, r2, r3) = self.pick_three_distinct(np, target_idx);
let mut mutant = Array1::zeros(ndim);
for d in 0..ndim {
mutant[d] =
population[[r1, d]] + f * (population[[r2, d]] - population[[r3, d]]);
}
mutant
}
DeStrategy::Best1 => {
let (r1, r2) = self.pick_two_distinct(np, target_idx);
let mut mutant = Array1::zeros(ndim);
for d in 0..ndim {
mutant[d] =
population[[best_idx, d]] + f * (population[[r1, d]] - population[[r2, d]]);
}
mutant
}
DeStrategy::RandToBest1 => {
let (r1, r2) = self.pick_two_distinct(np, target_idx);
let mut mutant = Array1::zeros(ndim);
for d in 0..ndim {
mutant[d] = population[[target_idx, d]]
+ f * (population[[best_idx, d]] - population[[target_idx, d]])
+ f * (population[[r1, d]] - population[[r2, d]]);
}
mutant
}
}
}
fn crossover(
&mut self,
target: &Array1<f64>,
mutant: &Array1<f64>,
ndim: usize,
) -> Array1<f64> {
match self.options.crossover {
CrossoverType::Binomial => self.binomial_crossover(target, mutant, ndim),
CrossoverType::Exponential => self.exponential_crossover(target, mutant, ndim),
}
}
fn binomial_crossover(
&mut self,
target: &Array1<f64>,
mutant: &Array1<f64>,
ndim: usize,
) -> Array1<f64> {
let cr = self.options.crossover_prob;
let j_rand = self.rng.random_range(0..ndim);
let mut trial = target.clone();
for d in 0..ndim {
if self.rng.random::<f64>() < cr || d == j_rand {
trial[d] = mutant[d];
}
}
trial
}
fn exponential_crossover(
&mut self,
target: &Array1<f64>,
mutant: &Array1<f64>,
ndim: usize,
) -> Array1<f64> {
let cr = self.options.crossover_prob;
let mut trial = target.clone();
let start = self.rng.random_range(0..ndim);
let mut d = start;
loop {
trial[d] = mutant[d];
d = (d + 1) % ndim;
if d == start || self.rng.random::<f64>() >= cr {
break;
}
}
trial
}
fn clip_to_bounds(&self, x: &Array1<f64>) -> Array1<f64> {
let mut clipped = x.clone();
for (d, (lo, hi)) in self.options.bounds.iter().enumerate() {
if d < clipped.len() {
clipped[d] = clipped[d].clamp(*lo, *hi);
}
}
clipped
}
fn penalized_fitness(&self, obj: f64, violation: f64) -> f64 {
if let Some(ref ch) = self.options.constraint_handler {
obj + ch.penalty_coeff * violation
} else {
obj
}
}
fn feasibility_selection(
&self,
trial_fit: f64,
trial_viol: f64,
current_fit: f64,
current_viol: f64,
) -> bool {
let trial_feasible = trial_viol <= 1e-15;
let current_feasible = current_viol <= 1e-15;
match (trial_feasible, current_feasible) {
(true, true) => trial_fit <= current_fit,
(true, false) => true, (false, true) => false, (false, false) => trial_viol < current_viol, }
}
fn find_best(&self, fitness: &[f64], violations: &[f64]) -> usize {
let mut best_idx = 0;
for i in 1..fitness.len() {
let is_better = if self
.options
.constraint_handler
.as_ref()
.map_or(false, |ch| ch.use_feasibility_rules)
{
self.feasibility_selection(
fitness[i],
violations[i],
fitness[best_idx],
violations[best_idx],
)
} else {
fitness[i] < fitness[best_idx]
};
if is_better {
best_idx = i;
}
}
best_idx
}
fn population_spread(&self, fitness: &[f64]) -> f64 {
if fitness.is_empty() {
return 0.0;
}
let mean = fitness.iter().sum::<f64>() / fitness.len() as f64;
let variance =
fitness.iter().map(|f| (f - mean).powi(2)).sum::<f64>() / fitness.len() as f64;
variance.sqrt()
}
fn pick_three_distinct(&mut self, np: usize, exclude: usize) -> (usize, usize, usize) {
let mut r1 = self.rng.random_range(0..np);
while r1 == exclude {
r1 = self.rng.random_range(0..np);
}
let mut r2 = self.rng.random_range(0..np);
while r2 == exclude || r2 == r1 {
r2 = self.rng.random_range(0..np);
}
let mut r3 = self.rng.random_range(0..np);
while r3 == exclude || r3 == r1 || r3 == r2 {
r3 = self.rng.random_range(0..np);
}
(r1, r2, r3)
}
fn pick_two_distinct(&mut self, np: usize, exclude: usize) -> (usize, usize) {
let mut r1 = self.rng.random_range(0..np);
while r1 == exclude {
r1 = self.rng.random_range(0..np);
}
let mut r2 = self.rng.random_range(0..np);
while r2 == exclude || r2 == r1 {
r2 = self.rng.random_range(0..np);
}
(r1, r2)
}
}
pub fn jde_optimize<F>(func: F, options: JdeOptions) -> OptimizeResult<DeResult>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
if options.base.bounds.is_empty() {
return Err(OptimizeError::InvalidInput(
"Bounds must be provided for jDE".to_string(),
));
}
let ndim = options.base.bounds.len();
let np = options.base.pop_size;
if np < 4 {
return Err(OptimizeError::InvalidParameter(
"Population size must be >= 4".to_string(),
));
}
let seed = options.base.seed.unwrap_or_else(|| rng().random());
let mut local_rng = StdRng::seed_from_u64(seed);
let bounds = &options.base.bounds;
let mut population = Array2::zeros((np, ndim));
for i in 0..np {
for d in 0..ndim {
let (lo, hi) = bounds[d];
population[[i, d]] = lo + local_rng.random::<f64>() * (hi - lo);
}
}
let mut f_vec = vec![options.base.mutation_factor; np];
let mut cr_vec = vec![options.base.crossover_prob; np];
let mut fitness: Vec<f64> = (0..np).map(|i| func(&population.row(i))).collect();
let mut nfev = np;
let mut best_idx = fitness
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
let mut best_x = population.row(best_idx).to_owned();
let mut best_fun = fitness[best_idx];
let mut no_improve: usize = 0;
for gen in 0..options.base.max_generations {
let mut new_pop = population.clone();
let mut new_fitness = fitness.clone();
let mut new_f_vec = f_vec.clone();
let mut new_cr_vec = cr_vec.clone();
for i in 0..np {
let fi = if local_rng.random::<f64>() < options.tau_f {
let new_f = options.f_lower + local_rng.random::<f64>() * options.f_upper;
new_f_vec[i] = new_f;
new_f
} else {
f_vec[i]
};
let cri = if local_rng.random::<f64>() < options.tau_cr {
let new_cr = local_rng.random::<f64>();
new_cr_vec[i] = new_cr;
new_cr
} else {
cr_vec[i]
};
let (r1, r2, r3) = pick_three_distinct_rng(&mut local_rng, np, i);
let mut mutant = Array1::zeros(ndim);
for d in 0..ndim {
mutant[d] = population[[r1, d]] + fi * (population[[r2, d]] - population[[r3, d]]);
}
let j_rand = local_rng.random_range(0..ndim);
let mut trial = Array1::zeros(ndim);
for d in 0..ndim {
if local_rng.random::<f64>() < cri || d == j_rand {
trial[d] = mutant[d];
} else {
trial[d] = population[[i, d]];
}
}
for d in 0..ndim {
let (lo, hi) = bounds[d];
trial[d] = trial[d].clamp(lo, hi);
}
let trial_f = func(&trial.view());
nfev += 1;
if trial_f <= fitness[i] {
for d in 0..ndim {
new_pop[[i, d]] = trial[d];
}
new_fitness[i] = trial_f;
} else {
new_f_vec[i] = f_vec[i];
new_cr_vec[i] = cr_vec[i];
}
}
population = new_pop;
fitness = new_fitness;
f_vec = new_f_vec;
cr_vec = new_cr_vec;
let new_best_idx = fitness
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
if fitness[new_best_idx] < best_fun {
best_idx = new_best_idx;
best_x = population.row(best_idx).to_owned();
best_fun = fitness[best_idx];
no_improve = 0;
} else {
no_improve += 1;
}
let spread = {
let mean = fitness.iter().sum::<f64>() / np as f64;
let var = fitness.iter().map(|f| (f - mean).powi(2)).sum::<f64>() / np as f64;
var.sqrt()
};
if spread < options.base.tol {
return Ok(DeResult {
x: best_x,
fun: best_fun,
nfev,
generations: gen + 1,
converged: true,
message: format!("jDE converged at generation {}", gen + 1),
population_spread: spread,
});
}
if no_improve >= options.base.patience {
return Ok(DeResult {
x: best_x,
fun: best_fun,
nfev,
generations: gen + 1,
converged: true,
message: format!(
"jDE: no improvement for {} generations",
options.base.patience
),
population_spread: spread,
});
}
}
let spread = {
let mean = fitness.iter().sum::<f64>() / np as f64;
let var = fitness.iter().map(|f| (f - mean).powi(2)).sum::<f64>() / np as f64;
var.sqrt()
};
Ok(DeResult {
x: best_x,
fun: best_fun,
nfev,
generations: options.base.max_generations,
converged: false,
message: "jDE completed max generations".to_string(),
population_spread: spread,
})
}
fn pick_three_distinct_rng(
rng_ref: &mut StdRng,
np: usize,
exclude: usize,
) -> (usize, usize, usize) {
let mut r1 = rng_ref.random_range(0..np);
while r1 == exclude {
r1 = rng_ref.random_range(0..np);
}
let mut r2 = rng_ref.random_range(0..np);
while r2 == exclude || r2 == r1 {
r2 = rng_ref.random_range(0..np);
}
let mut r3 = rng_ref.random_range(0..np);
while r3 == exclude || r3 == r1 || r3 == r2 {
r3 = rng_ref.random_range(0..np);
}
(r1, r2, r3)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
fn sphere(x: &ArrayView1<f64>) -> f64 {
x.iter().map(|xi| xi * xi).sum()
}
fn rosenbrock(x: &ArrayView1<f64>) -> f64 {
let mut sum = 0.0;
for i in 0..x.len() - 1 {
sum += 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2);
}
sum
}
fn rastrigin(x: &ArrayView1<f64>) -> 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>()
}
#[test]
fn test_de_rand1_sphere() {
let opts = DeOptions {
pop_size: 30,
max_generations: 500,
mutation_factor: 0.8,
crossover_prob: 0.9,
strategy: DeStrategy::Rand1,
crossover: CrossoverType::Binomial,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
patience: 200,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de.optimize(sphere).expect("DE should optimize sphere");
assert!(result.fun < 1e-4, "DE/rand/1 sphere: got {}", result.fun);
assert!(result.nfev > 0);
}
#[test]
fn test_de_best1_sphere() {
let opts = DeOptions {
pop_size: 30,
max_generations: 500,
mutation_factor: 0.5,
crossover_prob: 0.9,
strategy: DeStrategy::Best1,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
patience: 200,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de.optimize(sphere).expect("DE/best/1 should work");
assert!(result.fun < 1e-4, "DE/best/1 sphere: got {}", result.fun);
}
#[test]
fn test_de_rand_to_best1_sphere() {
let opts = DeOptions {
pop_size: 30,
max_generations: 500,
mutation_factor: 0.7,
crossover_prob: 0.9,
strategy: DeStrategy::RandToBest1,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
patience: 200,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de.optimize(sphere).expect("DE/rand-to-best/1 should work");
assert!(
result.fun < 1e-3,
"DE/rand-to-best/1 sphere: got {}",
result.fun
);
}
#[test]
fn test_de_exponential_crossover() {
let opts = DeOptions {
pop_size: 30,
max_generations: 500,
mutation_factor: 0.8,
crossover_prob: 0.9,
strategy: DeStrategy::Rand1,
crossover: CrossoverType::Exponential,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
patience: 200,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de.optimize(sphere).expect("exp crossover should work");
assert!(
result.fun < 0.1,
"Exponential crossover sphere: got {}",
result.fun
);
}
#[test]
fn test_de_rastrigin() {
let opts = DeOptions {
pop_size: 50,
max_generations: 1000,
mutation_factor: 0.8,
crossover_prob: 0.9,
strategy: DeStrategy::Rand1,
bounds: vec![(-5.12, 5.12); 3],
seed: Some(42),
patience: 300,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de.optimize(rastrigin).expect("DE on rastrigin");
assert!(result.fun < 10.0, "DE rastrigin: got {}", result.fun);
}
#[test]
fn test_jde_sphere() {
let opts = JdeOptions {
base: DeOptions {
pop_size: 30,
max_generations: 500,
mutation_factor: 0.5,
crossover_prob: 0.9,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
patience: 200,
..Default::default()
},
tau_f: 0.1,
tau_cr: 0.1,
f_lower: 0.1,
f_upper: 0.9,
};
let result = jde_optimize(sphere, opts).expect("jDE should work");
assert!(result.fun < 1e-4, "jDE sphere: got {}", result.fun);
}
#[test]
fn test_jde_rosenbrock() {
let opts = JdeOptions {
base: DeOptions {
pop_size: 40,
max_generations: 2000,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
patience: 500,
..Default::default()
},
..Default::default()
};
let result = jde_optimize(rosenbrock, opts).expect("jDE on rosenbrock");
assert!(result.fun < 1.0, "jDE rosenbrock: got {}", result.fun);
}
#[test]
fn test_de_with_constraints() {
let constraints = |x: &ArrayView1<f64>| -> Vec<f64> {
vec![2.0 - (x[0] + x[1])] };
let opts = DeOptions {
pop_size: 40,
max_generations: 500,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
patience: 200,
constraint_handler: Some(DeConstraintHandler {
penalty_coeff: 1e4,
use_feasibility_rules: true,
}),
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de
.optimize_constrained(sphere, Some(constraints))
.expect("constrained DE should work");
let sum = result.x[0] + result.x[1];
assert!(sum >= 1.5, "Constraint should be ~satisfied: sum = {}", sum);
assert!(result.fun < 5.0, "Constrained DE fun: {}", result.fun);
}
#[test]
fn test_de_opposition_based_init() {
let opts = DeOptions {
pop_size: 30,
max_generations: 300,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
opposition: OppositionBasedInit {
enabled: true,
jumping_rate: 0.3,
},
patience: 150,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de.optimize(sphere).expect("OBL DE should work");
assert!(result.fun < 1.0, "OBL DE sphere: got {}", result.fun);
}
#[test]
fn test_de_no_opposition() {
let opts = DeOptions {
pop_size: 30,
max_generations: 300,
bounds: vec![(-5.0, 5.0); 2],
seed: Some(42),
opposition: OppositionBasedInit {
enabled: false,
jumping_rate: 0.0,
},
patience: 150,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid options");
let result = de.optimize(sphere).expect("DE without OBL should work");
assert!(result.fun < 1.0, "DE no-OBL sphere: got {}", result.fun);
}
#[test]
fn test_de_empty_bounds_error() {
let opts = DeOptions {
bounds: vec![],
..Default::default()
};
let result = DifferentialEvolutionOptimizer::new(opts);
assert!(result.is_err());
}
#[test]
fn test_de_small_popsize_error() {
let opts = DeOptions {
pop_size: 2,
bounds: vec![(-1.0, 1.0)],
..Default::default()
};
let result = DifferentialEvolutionOptimizer::new(opts);
assert!(result.is_err());
}
#[test]
fn test_de_invalid_mutation_error() {
let opts = DeOptions {
mutation_factor: 3.0,
bounds: vec![(-1.0, 1.0)],
..Default::default()
};
let result = DifferentialEvolutionOptimizer::new(opts);
assert!(result.is_err());
}
#[test]
fn test_de_invalid_crossover_error() {
let opts = DeOptions {
crossover_prob: 1.5,
bounds: vec![(-1.0, 1.0)],
..Default::default()
};
let result = DifferentialEvolutionOptimizer::new(opts);
assert!(result.is_err());
}
#[test]
fn test_de_to_optimize_results() {
let de_result = DeResult {
x: array![1.0, 2.0],
fun: 5.0,
nfev: 1000,
generations: 50,
converged: true,
message: "test".to_string(),
population_spread: 0.01,
};
let opt = de_result.to_optimize_results();
assert_eq!(opt.nfev, 1000);
assert_eq!(opt.nit, 50);
assert!(opt.success);
}
#[test]
fn test_jde_empty_bounds_error() {
let opts = JdeOptions {
base: DeOptions {
bounds: vec![],
..Default::default()
},
..Default::default()
};
let result = jde_optimize(sphere, opts);
assert!(result.is_err());
}
#[test]
fn test_de_1d() {
let opts = DeOptions {
pop_size: 20,
max_generations: 200,
bounds: vec![(-10.0, 10.0)],
seed: Some(42),
patience: 100,
..Default::default()
};
let mut de = DifferentialEvolutionOptimizer::new(opts).expect("valid");
let result = de
.optimize(|x: &ArrayView1<f64>| (x[0] - 3.0).powi(2))
.expect("1D DE");
assert!(
(result.x[0] - 3.0).abs() < 0.5,
"1D DE: x = {}",
result.x[0]
);
}
}