1use std::marker::PhantomData;
24
25use burn::tensor::{Tensor, TensorData, backend::Backend};
26use rand::Rng;
27
28use crate::ops::mutation::gaussian_mutation_per_row;
29use crate::ops::replacement::{mu_comma_lambda, mu_plus_lambda};
30use crate::rng::{SeedPurpose, seed_stream};
31use crate::strategy::{Strategy, StrategyMetrics};
32
33#[derive(Debug, Clone, Copy)]
35pub enum EsKind {
36 OnePlusOne,
38 OnePlusLambda { lambda: usize },
40 MuCommaLambda { mu: usize, lambda: usize },
42 MuPlusLambda { mu: usize, lambda: usize },
44}
45
46impl EsKind {
47 #[must_use]
49 pub fn population_size(&self) -> usize {
50 match self {
51 EsKind::OnePlusOne => 1,
52 EsKind::OnePlusLambda { lambda }
53 | EsKind::MuCommaLambda { lambda, .. }
54 | EsKind::MuPlusLambda { lambda, .. } => *lambda,
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct EsConfig {
62 pub kind: EsKind,
64 pub genome_dim: usize,
66 pub bounds: (f32, f32),
68 pub initial_sigma: f32,
70 pub tau: f32,
73}
74
75impl EsConfig {
76 #[must_use]
78 pub fn default_for(kind: EsKind, genome_dim: usize) -> Self {
79 #[allow(clippy::cast_precision_loss)]
80 let d = genome_dim as f32;
81 let tau = 1.0 / (2.0 * d.sqrt()).sqrt();
82 Self {
83 kind,
84 genome_dim,
85 bounds: (-5.12, 5.12),
86 initial_sigma: 1.0,
87 tau,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct EsState<B: Backend> {
95 pub parents: Tensor<B, 2>,
98 pub sigmas: Tensor<B, 1>,
105 pub parent_fitness: Vec<f32>,
107 pub best_genome: Option<Tensor<B, 2>>,
109 pub best_fitness: f32,
111 pub generation: usize,
113 pub successes_in_window: u32,
115 pub window_len: u32,
117}
118
119#[derive(Debug, Clone, Copy, Default)]
132pub struct EvolutionStrategy<B: Backend> {
133 _backend: PhantomData<fn() -> B>,
134}
135
136impl<B: Backend> EvolutionStrategy<B> {
137 #[must_use]
139 pub fn new() -> Self {
140 Self {
141 _backend: PhantomData,
142 }
143 }
144
145 fn mu(kind: EsKind) -> usize {
146 match kind {
147 EsKind::OnePlusOne | EsKind::OnePlusLambda { .. } => 1,
148 EsKind::MuCommaLambda { mu, .. } | EsKind::MuPlusLambda { mu, .. } => mu,
149 }
150 }
151
152 fn sample_initial_parents(
153 params: &EsConfig,
154 rng: &mut dyn Rng,
155 device: &B::Device,
156 ) -> (Tensor<B, 2>, Tensor<B, 1>) {
157 let mu = Self::mu(params.kind);
158 let (lo, hi) = params.bounds;
159 B::seed(device, rng.next_u64());
160 let parents = Tensor::<B, 2>::random(
161 [mu, params.genome_dim],
162 burn::tensor::Distribution::Uniform(f64::from(lo), f64::from(hi)),
163 device,
164 );
165 let sigmas = Tensor::<B, 1>::from_data(
166 TensorData::new(vec![params.initial_sigma; mu], [mu]),
167 device,
168 );
169 (parents, sigmas)
170 }
171}
172
173impl<B: Backend> Strategy<B> for EvolutionStrategy<B>
174where
175 B::Device: Clone,
176{
177 type Params = EsConfig;
178 type State = EsState<B>;
179 type Genome = Tensor<B, 2>;
180
181 fn init(&self, params: &EsConfig, rng: &mut dyn Rng, device: &B::Device) -> EsState<B> {
182 let (parents, sigmas) = Self::sample_initial_parents(params, rng, device);
183 EsState {
184 parents,
185 sigmas,
186 parent_fitness: Vec::new(),
187 best_genome: None,
188 best_fitness: f32::INFINITY,
189 generation: 0,
190 successes_in_window: 0,
191 window_len: 0,
192 }
193 }
194
195 fn ask(
196 &self,
197 params: &EsConfig,
198 state: &EsState<B>,
199 rng: &mut dyn Rng,
200 device: &B::Device,
201 ) -> (Tensor<B, 2>, EsState<B>) {
202 if state.parent_fitness.is_empty() {
205 return (state.parents.clone(), state.clone());
206 }
207
208 let lambda = params.kind.population_size();
209 let mu = Self::mu(params.kind);
210
211 let mut mutation_rng = seed_stream(
212 rng.next_u64(),
213 state.generation as u64,
214 SeedPurpose::Mutation,
215 );
216 let mut sigma_rng =
217 seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
218
219 let mut parent_indices: Vec<i64> = Vec::with_capacity(lambda);
224 {
225 use rand::RngExt;
226 for _ in 0..lambda {
227 #[allow(clippy::cast_possible_wrap)]
228 parent_indices.push(sigma_rng.random_range(0..mu) as i64);
229 }
230 }
231 let idx_tensor = Tensor::<B, 1, burn::tensor::Int>::from_data(
232 TensorData::new(parent_indices.clone(), [lambda]),
233 device,
234 );
235 let duplicated_parents = state.parents.clone().select(0, idx_tensor.clone());
236 let duplicated_sigmas = state.sigmas.clone().select(0, idx_tensor);
237
238 let is_one_plus = matches!(
241 params.kind,
242 EsKind::OnePlusOne | EsKind::OnePlusLambda { .. }
243 );
244 let offspring_sigmas = if is_one_plus {
245 duplicated_sigmas
246 } else {
247 B::seed(device, sigma_rng.next_u64());
248 let noise = Tensor::<B, 1>::random(
249 [lambda],
250 burn::tensor::Distribution::Normal(0.0, 1.0),
251 device,
252 );
253 duplicated_sigmas * noise.mul_scalar(params.tau).exp()
254 };
255
256 B::seed(device, mutation_rng.next_u64());
258 let mutated =
259 gaussian_mutation_per_row(duplicated_parents, offspring_sigmas.clone(), device);
260
261 let (lo, hi) = params.bounds;
263 let mutated = mutated.clamp(lo, hi);
264
265 let mut state = state.clone();
266 let combined_sigmas = Tensor::cat(vec![state.sigmas.clone(), offspring_sigmas], 0);
274 state.sigmas = combined_sigmas;
275 (mutated, state)
276 }
277
278 #[allow(clippy::too_many_lines)]
279 fn tell(
280 &self,
281 params: &EsConfig,
282 offspring: Tensor<B, 2>,
283 fitness: Tensor<B, 1>,
284 mut state: EsState<B>,
285 _rng: &mut dyn Rng,
286 ) -> (EsState<B>, StrategyMetrics) {
287 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
288
289 if state.parent_fitness.is_empty() {
292 state.parent_fitness.clone_from(&fitness_host);
293 state.generation += 1;
294 update_best(&mut state, &offspring, &fitness_host);
295 let m = StrategyMetrics::from_host_fitness(
296 state.generation,
297 &fitness_host,
298 state.best_fitness,
299 );
300 state.best_fitness = m.best_fitness_ever;
301 state.parents = offspring;
302 let mu = Self::mu(params.kind);
304 let device = state.parents.device();
305 state.sigmas = Tensor::<B, 1>::from_data(
306 TensorData::new(vec![params.initial_sigma; mu], [mu]),
307 &device,
308 );
309 return (state, m);
310 }
311
312 let device = offspring.device();
313 let mu = Self::mu(params.kind);
314 let lambda = params.kind.population_size();
317 #[allow(clippy::single_range_in_vec_init)]
318 let parent_sigmas = state.sigmas.clone().slice([0..mu]);
319 #[allow(clippy::single_range_in_vec_init)]
320 let offspring_sigmas = state.sigmas.clone().slice([mu..(mu + lambda)]);
321
322 match params.kind {
323 EsKind::OnePlusOne => {
324 let parent_fit = state.parent_fitness[0];
326 let offspring_fit = fitness_host[0];
327 let success = offspring_fit < parent_fit;
328 state.window_len += 1;
329 if success {
330 state.successes_in_window += 1;
331 state.parents.clone_from(&offspring);
332 state.parent_fitness = vec![offspring_fit];
333 }
334 #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
336 let window = 10_u32.saturating_mul(params.genome_dim as u32).max(1);
337 if state.window_len >= window {
338 #[allow(clippy::cast_precision_loss)]
339 let rate = state.successes_in_window as f32 / state.window_len as f32;
340 let current_sigma =
341 state.sigmas.clone().into_data().into_vec::<f32>().unwrap()[0];
342 let new_sigma = if rate > 0.2 {
343 current_sigma * 1.22
344 } else if rate < 0.2 {
345 current_sigma / 1.22
346 } else {
347 current_sigma
348 };
349 state.sigmas =
350 Tensor::<B, 1>::from_data(TensorData::new(vec![new_sigma], [1]), &device);
351 state.successes_in_window = 0;
352 state.window_len = 0;
353 } else {
354 state.sigmas = parent_sigmas;
355 }
356 }
357 EsKind::OnePlusLambda { .. } => {
358 let best_off_idx = argmin(&fitness_host);
360 let best_off_fit = fitness_host[best_off_idx];
361 if best_off_fit < state.parent_fitness[0] {
362 #[allow(clippy::single_range_in_vec_init)]
363 let best_row = offspring.clone().slice([best_off_idx..best_off_idx + 1]);
364 state.parents = best_row;
365 state.parent_fitness = vec![best_off_fit];
366 }
367 state.sigmas = parent_sigmas;
368 }
369 EsKind::MuCommaLambda { mu, .. } => {
370 let (survivors, survivor_f) =
371 mu_comma_lambda::<B>(offspring.clone(), &fitness_host, mu, &device);
372 let survivor_idx =
374 crate::ops::selection::truncation_indices_host(&fitness_host, mu);
375 let survivor_sigmas = offspring_sigmas.select(
376 0,
377 Tensor::<B, 1, burn::tensor::Int>::from_data(
378 TensorData::new(survivor_idx, [mu]),
379 &device,
380 ),
381 );
382 state.parents = survivors;
383 state.parent_fitness = survivor_f;
384 state.sigmas = survivor_sigmas;
385 }
386 EsKind::MuPlusLambda { mu, .. } => {
387 let (survivors, survivor_f) = mu_plus_lambda::<B>(
388 state.parents.clone(),
389 &state.parent_fitness,
390 offspring.clone(),
391 &fitness_host,
392 mu,
393 &device,
394 );
395 let combined_f: Vec<f32> = state
397 .parent_fitness
398 .iter()
399 .chain(fitness_host.iter())
400 .copied()
401 .collect();
402 let survivor_idx = crate::ops::selection::truncation_indices_host(&combined_f, mu);
403 let combined_sigmas = Tensor::cat(vec![parent_sigmas, offspring_sigmas], 0);
404 let survivor_sigmas = combined_sigmas.select(
405 0,
406 Tensor::<B, 1, burn::tensor::Int>::from_data(
407 TensorData::new(survivor_idx, [mu]),
408 &device,
409 ),
410 );
411 state.parents = survivors;
412 state.parent_fitness = survivor_f;
413 state.sigmas = survivor_sigmas;
414 }
415 }
416
417 state.generation += 1;
418 update_best(&mut state, &offspring, &fitness_host);
419 let m =
420 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
421 state.best_fitness = m.best_fitness_ever;
422 (state, m)
423 }
424
425 fn best(&self, state: &EsState<B>) -> Option<(Tensor<B, 2>, f32)> {
426 state
427 .best_genome
428 .as_ref()
429 .map(|g| (g.clone(), state.best_fitness))
430 }
431}
432
433fn argmin(xs: &[f32]) -> usize {
434 let mut best_idx = 0usize;
435 let mut best = f32::INFINITY;
436 for (i, &v) in xs.iter().enumerate() {
437 if v < best {
438 best = v;
439 best_idx = i;
440 }
441 }
442 best_idx
443}
444
445fn update_best<B: Backend>(state: &mut EsState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
446 if fitness.is_empty() {
447 return;
448 }
449 let best_idx = argmin(fitness);
450 let best_f = fitness[best_idx];
451 if best_f < state.best_fitness {
452 let device = pop.device();
453 #[allow(clippy::cast_possible_wrap)]
454 let idx = Tensor::<B, 1, burn::tensor::Int>::from_data(
455 TensorData::new(vec![best_idx as i64], [1]),
456 &device,
457 );
458 state.best_genome = Some(pop.clone().select(0, idx));
459 state.best_fitness = best_f;
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::fitness::FromFitnessEvaluable;
467 use crate::strategy::EvolutionaryHarness;
468 use burn::backend::NdArray;
469 use rlevo_core::fitness::FitnessEvaluable;
470 type TestBackend = NdArray;
471
472 struct Sphere;
473 struct SphereFit;
474 impl FitnessEvaluable for SphereFit {
475 type Individual = Vec<f64>;
476 type Landscape = Sphere;
477 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
478 x.iter().map(|v| v * v).sum()
479 }
480 }
481
482 fn run_es(kind: EsKind, dim: usize, generations: usize, seed: u64) -> f32 {
483 let device = Default::default();
484 let strategy = EvolutionStrategy::<TestBackend>::new();
485 let params = EsConfig::default_for(kind, dim);
486 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
487 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
488 strategy,
489 params,
490 fitness_fn,
491 seed,
492 device,
493 generations,
494 );
495 harness.reset();
496 loop {
497 let step = harness.step(());
498 if step.done {
499 break;
500 }
501 }
502 harness.latest_metrics().unwrap().best_fitness_ever
503 }
504
505 #[test]
506 fn one_plus_lambda_converges_on_sphere_d2() {
507 let best = run_es(EsKind::OnePlusLambda { lambda: 8 }, 2, 200, 7);
508 assert!(best < 1e-2, "OnePlusLambda best={best}");
509 }
510
511 #[test]
512 fn one_plus_one_converges_on_sphere_d2() {
513 let best = run_es(EsKind::OnePlusOne, 2, 500, 11);
514 assert!(best < 1e-2, "OnePlusOne best={best}");
515 }
516
517 #[test]
518 fn mu_plus_lambda_converges_on_sphere_d2() {
519 let best = run_es(EsKind::MuPlusLambda { mu: 3, lambda: 8 }, 2, 200, 7);
520 assert!(best < 1e-2, "MuPlusLambda best={best}");
521 }
522
523 #[test]
524 fn mu_comma_lambda_converges_on_sphere_d2() {
525 let best = run_es(EsKind::MuCommaLambda { mu: 3, lambda: 8 }, 2, 200, 7);
526 assert!(best < 1e-1, "MuCommaLambda best={best}");
527 }
528
529 #[test]
530 fn mu_plus_lambda_converges_on_sphere_d10() {
531 let best = run_es(EsKind::MuPlusLambda { mu: 5, lambda: 20 }, 10, 1500, 42);
536 assert!(best < 1e-6, "MuPlusLambda D10 best={best}");
537 }
538}