use numr::dtype::DType;
use numr::error::Result;
use numr::ops::{CompareOps, RandomOps, ScalarOps, TensorOps};
use numr::runtime::{Runtime, RuntimeClient};
use numr::tensor::Tensor;
use crate::optimize::error::{OptimizeError, OptimizeResult};
use crate::optimize::global::GlobalOptions;
use super::{clamp_to_bounds, validate_bounds};
#[derive(Debug, Clone)]
pub struct DifferentialEvolutionTensorResult<R: Runtime<DType = DType>> {
pub x: Tensor<R>,
pub fun: f64,
pub iterations: usize,
pub nfev: usize,
pub converged: bool,
}
pub fn differential_evolution_impl<R, C, F>(
client: &C,
f: F,
lower_bounds: &Tensor<R>,
upper_bounds: &Tensor<R>,
options: &GlobalOptions,
) -> OptimizeResult<DifferentialEvolutionTensorResult<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + CompareOps<R> + RandomOps<R> + RuntimeClient<R>,
F: Fn(&Tensor<R>) -> Result<f64>,
{
let n = lower_bounds.shape()[0];
if n == 0 {
return Err(OptimizeError::InvalidInput {
context: "differential_evolution: empty bounds".to_string(),
});
}
validate_bounds(client, lower_bounds, upper_bounds)?;
let pop_size = (15 * n).max(25);
let f_scale = 0.8;
let cr = 0.9;
let bounds_range =
client
.sub(upper_bounds, lower_bounds)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: bounds range - {}", e),
})?;
let mut population = init_population(client, lower_bounds, &bounds_range, pop_size, n)?;
let mut fitness = evaluate_population(&f, &population)?;
let mut nfev = pop_size;
let (mut best_idx, mut best_fitness) = find_best(&fitness);
let indices = client.arange(0.0, n as f64, 1.0, DType::F64).map_err(|e| {
OptimizeError::NumericalError {
message: format!("de: create indices - {}", e),
}
})?;
for iter in 0..options.max_iter {
let fitness_range = compute_fitness_range(&fitness);
if fitness_range < options.tol {
return Ok(DifferentialEvolutionTensorResult {
x: population[best_idx].clone(),
fun: best_fitness,
iterations: iter + 1,
nfev,
converged: true,
});
}
for i in 0..pop_size {
let (r0, r1, r2) = select_random_indices::<R, C>(client, pop_size, i)?;
let x_r0 = &population[r0];
let x_r1 = &population[r1];
let x_r2 = &population[r2];
let x_i = &population[i];
let diff = client
.sub(x_r1, x_r2)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: diff - {}", e),
})?;
let scaled_diff =
client
.mul_scalar(&diff, f_scale)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: scaled diff - {}", e),
})?;
let mutant_unclamped =
client
.add(x_r0, &scaled_diff)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: mutant - {}", e),
})?;
let mutant = clamp_to_bounds(client, &mutant_unclamped, lower_bounds, upper_bounds)?;
let trial = crossover(client, x_i, &mutant, cr, n, &indices)?;
let trial_fitness = f(&trial).map_err(|e| OptimizeError::NumericalError {
message: format!("de: evaluation - {}", e),
})?;
nfev += 1;
if trial_fitness <= fitness[i] {
population[i] = trial;
fitness[i] = trial_fitness;
if trial_fitness < best_fitness {
best_fitness = trial_fitness;
best_idx = i;
}
}
}
}
Ok(DifferentialEvolutionTensorResult {
x: population[best_idx].clone(),
fun: best_fitness,
iterations: options.max_iter,
nfev,
converged: false,
})
}
fn init_population<R, C>(
client: &C,
lower: &Tensor<R>,
range: &Tensor<R>,
pop_size: usize,
n: usize,
) -> OptimizeResult<Vec<Tensor<R>>>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + RandomOps<R> + RuntimeClient<R>,
{
let mut population = Vec::with_capacity(pop_size);
for _ in 0..pop_size {
let rand_ind =
client
.rand(&[n], DType::F64)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: rand individual - {}", e),
})?;
let scaled = client
.mul(&rand_ind, range)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: scale individual - {}", e),
})?;
let individual = client
.add(lower, &scaled)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: init individual - {}", e),
})?;
population.push(individual);
}
Ok(population)
}
fn evaluate_population<R, F>(f: &F, population: &[Tensor<R>]) -> OptimizeResult<Vec<f64>>
where
R: Runtime<DType = DType>,
F: Fn(&Tensor<R>) -> Result<f64>,
{
let mut fitness = Vec::with_capacity(population.len());
for individual in population {
let fit = f(individual).map_err(|e| OptimizeError::NumericalError {
message: format!("de: initial evaluation - {}", e),
})?;
fitness.push(fit);
}
Ok(fitness)
}
fn crossover<R, C>(
client: &C,
target: &Tensor<R>,
mutant: &Tensor<R>,
cr: f64,
n: usize,
indices: &Tensor<R>,
) -> OptimizeResult<Tensor<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + CompareOps<R> + RandomOps<R> + RuntimeClient<R>,
{
let rand_mask = client
.rand(&[n], DType::F64)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: crossover rand - {}", e),
})?;
let cr_tensor =
client
.fill(&[n], cr, DType::F64)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: cr tensor - {}", e),
})?;
let lt_mask = client
.lt(&rand_mask, &cr_tensor)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: lt comparison - {}", e),
})?;
let rand_idx = client
.rand(&[1], DType::F64)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: j_rand - {}", e),
})?;
let j_rand_val: Vec<f64> = rand_idx.to_vec();
let j_rand = (j_rand_val[0] * n as f64) as usize;
let j_rand_tensor = client.fill(&[n], j_rand as f64, DType::F64).map_err(|e| {
OptimizeError::NumericalError {
message: format!("de: j_rand tensor - {}", e),
}
})?;
let j_rand_mask =
client
.eq(indices, &j_rand_tensor)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: j_rand mask - {}", e),
})?;
let combined_f64 =
client
.maximum(<_mask, &j_rand_mask)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: combine masks - {}", e),
})?;
let combined_mask =
client
.cast(&combined_f64, DType::U8)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: cast mask - {}", e),
})?;
client
.where_cond(&combined_mask, mutant, target)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: crossover select - {}", e),
})
}
fn select_random_indices<R, C>(
client: &C,
pop_size: usize,
exclude: usize,
) -> OptimizeResult<(usize, usize, usize)>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + RandomOps<R> + RuntimeClient<R>,
{
let rand_vals = client
.rand(&[6], DType::F64)
.map_err(|e| OptimizeError::NumericalError {
message: format!("de: select random - {}", e),
})?;
let vals: Vec<f64> = rand_vals.to_vec();
let mut selected = Vec::with_capacity(3);
let mut idx = 0;
while selected.len() < 3 && idx < 6 {
let candidate = (vals[idx] * pop_size as f64) as usize % pop_size;
if candidate != exclude && !selected.contains(&candidate) {
selected.push(candidate);
}
idx += 1;
}
if selected.len() < 3 {
for k in 0..pop_size {
if k != exclude && !selected.contains(&k) {
selected.push(k);
if selected.len() >= 3 {
break;
}
}
}
}
Ok((selected[0], selected[1], selected[2]))
}
fn find_best(fitness: &[f64]) -> (usize, f64) {
let mut best_idx = 0;
let mut best_val = fitness[0];
for (i, &f) in fitness.iter().enumerate() {
if f < best_val {
best_val = f;
best_idx = i;
}
}
(best_idx, best_val)
}
fn compute_fitness_range(fitness: &[f64]) -> f64 {
let max = fitness.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min = fitness.iter().cloned().fold(f64::INFINITY, f64::min);
max - min
}