1use std::marker::PhantomData;
33
34use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
35use rand::{Rng, RngExt};
36
37use crate::rng::{SeedPurpose, seed_stream};
38use crate::strategy::{Strategy, StrategyMetrics};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum DeVariant {
59 Rand1Bin,
63 Best1Bin,
72 CurrentToBest1Bin,
80 Rand2Bin,
84 Rand1Exp,
88}
89
90impl DeVariant {
91 const fn random_indices(self) -> usize {
94 match self {
95 DeVariant::Rand1Bin | DeVariant::Rand1Exp => 3,
96 DeVariant::Best1Bin | DeVariant::CurrentToBest1Bin => 2,
97 DeVariant::Rand2Bin => 5,
98 }
99 }
100
101 const fn is_exponential(self) -> bool {
103 matches!(self, DeVariant::Rand1Exp)
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct DeConfig {
110 pub pop_size: usize,
112 pub genome_dim: usize,
114 pub bounds: (f32, f32),
116 pub f: f32,
118 pub cr: f32,
120 pub variant: DeVariant,
122}
123
124impl DeConfig {
125 #[must_use]
128 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
129 Self {
130 pop_size,
131 genome_dim,
132 bounds: (-5.12, 5.12),
133 f: 0.5,
134 cr: 0.9,
135 variant: DeVariant::Rand1Bin,
136 }
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct DeState<B: Backend> {
143 pub population: Tensor<B, 2>,
145 pub fitness: Vec<f32>,
147 pub best_index: usize,
149 pub best_genome: Option<Tensor<B, 2>>,
151 pub best_fitness: f32,
153 pub generation: usize,
155}
156
157#[derive(Debug, Clone, Copy, Default)]
171pub struct DifferentialEvolution<B: Backend> {
172 _backend: PhantomData<fn() -> B>,
173}
174
175impl<B: Backend> DifferentialEvolution<B> {
176 #[must_use]
178 pub fn new() -> Self {
179 Self {
180 _backend: PhantomData,
181 }
182 }
183
184 fn sample_initial_population(
185 params: &DeConfig,
186 rng: &mut dyn Rng,
187 device: &B::Device,
188 ) -> Tensor<B, 2> {
189 let (lo, hi) = params.bounds;
190 B::seed(device, rng.next_u64());
191 Tensor::<B, 2>::random(
192 [params.pop_size, params.genome_dim],
193 burn::tensor::Distribution::Uniform(f64::from(lo), f64::from(hi)),
194 device,
195 )
196 }
197
198 fn sample_distinct_excluding(
206 self_idx: usize,
207 pop_size: usize,
208 k: usize,
209 rng: &mut dyn Rng,
210 ) -> Vec<usize> {
211 assert!(
212 pop_size > k,
213 "DE: pop_size must exceed the number of distinct indices required"
214 );
215 let mut chosen = Vec::with_capacity(k);
216 while chosen.len() < k {
217 let candidate = rng.random_range(0..pop_size);
218 if candidate != self_idx && !chosen.contains(&candidate) {
219 chosen.push(candidate);
220 }
221 }
222 chosen
223 }
224}
225
226impl<B: Backend> Strategy<B> for DifferentialEvolution<B>
227where
228 B::Device: Clone,
229{
230 type Params = DeConfig;
231 type State = DeState<B>;
232 type Genome = Tensor<B, 2>;
233
234 fn init(&self, params: &DeConfig, rng: &mut dyn Rng, device: &B::Device) -> DeState<B> {
235 let population = Self::sample_initial_population(params, rng, device);
236 DeState {
237 population,
238 fitness: Vec::new(),
239 best_index: 0,
240 best_genome: None,
241 best_fitness: f32::INFINITY,
242 generation: 0,
243 }
244 }
245
246 #[allow(clippy::too_many_lines, clippy::many_single_char_names)]
247 fn ask(
248 &self,
249 params: &DeConfig,
250 state: &DeState<B>,
251 rng: &mut dyn Rng,
252 device: &B::Device,
253 ) -> (Tensor<B, 2>, DeState<B>) {
254 if state.fitness.is_empty() {
256 return (state.population.clone(), state.clone());
257 }
258
259 let DeConfig {
260 pop_size,
261 genome_dim,
262 f,
263 cr,
264 variant,
265 ..
266 } = *params;
267
268 let mut trial_rng =
269 seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Trial);
270
271 let k = variant.random_indices();
277 let mut rand_indices: Vec<Vec<usize>> =
278 (0..k).map(|_| Vec::with_capacity(pop_size)).collect();
279 for i in 0..pop_size {
280 let chosen = Self::sample_distinct_excluding(i, pop_size, k, &mut trial_rng);
281 for (j, idx) in chosen.into_iter().enumerate() {
282 rand_indices[j].push(idx);
283 }
284 }
285
286 let gather = |idxs: &[usize]| -> Tensor<B, 2> {
287 #[allow(clippy::cast_possible_wrap)]
288 let v: Vec<i64> = idxs.iter().map(|&i| i as i64).collect();
289 let t = Tensor::<B, 1, Int>::from_data(TensorData::new(v, [pop_size]), device);
290 state.population.clone().select(0, t)
291 };
292
293 let v = match variant {
294 DeVariant::Rand1Bin | DeVariant::Rand1Exp => {
295 let a = gather(&rand_indices[0]);
296 let b = gather(&rand_indices[1]);
297 let c = gather(&rand_indices[2]);
298 a + (b - c).mul_scalar(f)
299 }
300 DeVariant::Best1Bin => {
301 #[allow(clippy::single_range_in_vec_init)]
302 let best = state
303 .population
304 .clone()
305 .slice([state.best_index..state.best_index + 1])
306 .expand([pop_size, genome_dim]);
307 let b = gather(&rand_indices[0]);
308 let c = gather(&rand_indices[1]);
309 best + (b - c).mul_scalar(f)
310 }
311 DeVariant::CurrentToBest1Bin => {
312 #[allow(clippy::single_range_in_vec_init)]
313 let best = state
314 .population
315 .clone()
316 .slice([state.best_index..state.best_index + 1])
317 .expand([pop_size, genome_dim]);
318 let current = state.population.clone();
319 let a = gather(&rand_indices[0]);
320 let b = gather(&rand_indices[1]);
321 current.clone() + (best - current).mul_scalar(f) + (a - b).mul_scalar(f)
322 }
323 DeVariant::Rand2Bin => {
324 let a = gather(&rand_indices[0]);
325 let b = gather(&rand_indices[1]);
326 let c = gather(&rand_indices[2]);
327 let d = gather(&rand_indices[3]);
328 let e = gather(&rand_indices[4]);
329 a + (b - c).mul_scalar(f) + (d - e).mul_scalar(f)
330 }
331 };
332
333 let mut cross_rng = seed_stream(
338 rng.next_u64(),
339 state.generation as u64,
340 SeedPurpose::Crossover,
341 );
342 let mut cross_mask = vec![false; pop_size * genome_dim];
343 if variant.is_exponential() {
344 for row in 0..pop_size {
345 let start = cross_rng.random_range(0..genome_dim);
346 let mut len = 1;
347 while len < genome_dim && cross_rng.random::<f32>() < cr {
348 len += 1;
349 }
350 for k in 0..len {
351 let j = (start + k) % genome_dim;
352 cross_mask[row * genome_dim + j] = true;
353 }
354 }
355 } else {
356 for row in 0..pop_size {
357 let j_rand = cross_rng.random_range(0..genome_dim);
358 for j in 0..genome_dim {
359 if j == j_rand || cross_rng.random::<f32>() < cr {
360 cross_mask[row * genome_dim + j] = true;
361 }
362 }
363 }
364 }
365 #[allow(clippy::cast_possible_wrap)]
366 let mask_int: Vec<i64> = cross_mask.iter().map(|&b| i64::from(b)).collect();
367 let mask_tensor = Tensor::<B, 2, Int>::from_data(
368 TensorData::new(mask_int, [pop_size, genome_dim]),
369 device,
370 );
371 let mask_bool = mask_tensor.equal_elem(1);
372
373 let trial = state.population.clone().mask_where(mask_bool, v);
375 let (lo, hi) = params.bounds;
376 let trial = trial.clamp(lo, hi);
377
378 (trial, state.clone())
379 }
380
381 fn tell(
382 &self,
383 _params: &DeConfig,
384 trial: Tensor<B, 2>,
385 fitness: Tensor<B, 1>,
386 mut state: DeState<B>,
387 _rng: &mut dyn Rng,
388 ) -> (DeState<B>, StrategyMetrics) {
389 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
390
391 if state.fitness.is_empty() {
393 state.fitness.clone_from(&fitness_host);
394 state.best_index = argmin(&fitness_host);
395 state.generation += 1;
396 update_best(&mut state, &trial, &fitness_host);
397 let m = StrategyMetrics::from_host_fitness(
398 state.generation,
399 &fitness_host,
400 state.best_fitness,
401 );
402 state.best_fitness = m.best_fitness_ever;
403 state.population = trial;
404 return (state, m);
405 }
406
407 let device = trial.device();
410 let pop_size = state.fitness.len();
411 let mut replace_mask = vec![0i64; pop_size];
412 let mut new_fit = state.fitness.clone();
413 for i in 0..pop_size {
414 if fitness_host[i] <= state.fitness[i] {
415 replace_mask[i] = 1;
416 new_fit[i] = fitness_host[i];
417 }
418 }
419
420 let mask_int =
421 Tensor::<B, 1, Int>::from_data(TensorData::new(replace_mask, [pop_size]), &device);
422 let mask_bool_row = mask_int.equal_elem(1);
423 let genome_dim = state.population.shape().dims[1];
424 let mask_bool = mask_bool_row
425 .unsqueeze_dim::<2>(1)
426 .expand([pop_size, genome_dim]);
427 let next_pop = state
428 .population
429 .clone()
430 .mask_where(mask_bool, trial.clone());
431
432 state.population = next_pop;
433 state.fitness.clone_from(&new_fit);
434 state.best_index = argmin(&new_fit);
435 state.generation += 1;
436 update_best(&mut state, &trial, &fitness_host);
437 let m = StrategyMetrics::from_host_fitness(state.generation, &new_fit, state.best_fitness);
438 state.best_fitness = m.best_fitness_ever;
439 (state, m)
440 }
441
442 fn best(&self, state: &DeState<B>) -> Option<(Tensor<B, 2>, f32)> {
443 state
444 .best_genome
445 .as_ref()
446 .map(|g| (g.clone(), state.best_fitness))
447 }
448}
449
450fn argmin(xs: &[f32]) -> usize {
451 let mut best_idx = 0usize;
452 let mut best = f32::INFINITY;
453 for (i, &v) in xs.iter().enumerate() {
454 if v < best {
455 best = v;
456 best_idx = i;
457 }
458 }
459 best_idx
460}
461
462fn update_best<B: Backend>(state: &mut DeState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
463 if fitness.is_empty() {
464 return;
465 }
466 let best_idx = argmin(fitness);
467 let best_f = fitness[best_idx];
468 if best_f < state.best_fitness {
469 let device = pop.device();
470 #[allow(clippy::cast_possible_wrap)]
471 let idx =
472 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![best_idx as i64], [1]), &device);
473 state.best_genome = Some(pop.clone().select(0, idx));
474 state.best_fitness = best_f;
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::fitness::FromFitnessEvaluable;
482 use crate::strategy::EvolutionaryHarness;
483 use burn::backend::NdArray;
484 use rlevo_core::fitness::FitnessEvaluable;
485 type TestBackend = NdArray;
486
487 struct Sphere;
488 struct SphereFit;
489 impl FitnessEvaluable for SphereFit {
490 type Individual = Vec<f64>;
491 type Landscape = Sphere;
492 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
493 x.iter().map(|v| v * v).sum()
494 }
495 }
496
497 fn run_de(variant: DeVariant, dim: usize, gens: usize) -> f32 {
498 let device = Default::default();
499 let mut params = DeConfig::default_for(30, dim);
500 params.variant = variant;
501 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
502 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
503 DifferentialEvolution::<TestBackend>::new(),
504 params,
505 fitness_fn,
506 11,
507 device,
508 gens,
509 );
510 harness.reset();
511 loop {
512 if harness.step(()).done {
513 break;
514 }
515 }
516 harness.latest_metrics().unwrap().best_fitness_ever
517 }
518
519 #[test]
531 fn all_variants_converge_on_sphere_d10() {
532 let rand1bin = run_de(DeVariant::Rand1Bin, 10, 500);
533 assert!(rand1bin < 1e-6, "DE/rand/1/bin best={rand1bin}");
534
535 let rand2bin = run_de(DeVariant::Rand2Bin, 10, 800);
536 assert!(rand2bin < 1e-6, "DE/rand/2/bin best={rand2bin}");
537
538 let rand1exp = run_de(DeVariant::Rand1Exp, 10, 500);
539 assert!(rand1exp < 1e-6, "DE/rand/1/exp best={rand1exp}");
540
541 let best1bin = run_de(DeVariant::Best1Bin, 10, 500);
542 assert!(best1bin < 1.0, "DE/best/1/bin best={best1bin}");
543
544 let c2b = run_de(DeVariant::CurrentToBest1Bin, 10, 500);
545 assert!(c2b < 2.0, "DE/current-to-best/1/bin best={c2b}");
546 }
547}