1use std::marker::PhantomData;
40
41use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
42use rand::Rng;
43use rand::RngExt;
44
45use crate::ops::crossover::binary_uniform_crossover;
46use crate::ops::mutation::bit_flip_mutation;
47use crate::ops::selection::{tournament_indices_host, truncation_indices_host};
48use crate::rng::{SeedPurpose, seed_stream};
49use crate::strategy::{Strategy, StrategyMetrics};
50use rlevo_core::config::{self, ConfigError, ConstraintKind, Validate};
51use rlevo_core::probability::Probability;
52
53#[derive(Debug, Clone)]
55pub struct BinaryGaConfig {
56 pub pop_size: usize,
58 pub genome_dim: usize,
60 pub mutation_rate: Probability,
62 pub crossover_p: Probability,
65 pub tournament_size: usize,
67 pub elitism_k: usize,
69}
70
71impl BinaryGaConfig {
72 #[must_use]
84 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
85 Self {
86 pop_size,
87 genome_dim,
88 #[allow(clippy::cast_precision_loss)]
89 mutation_rate: Probability::new(1.0 / genome_dim as f32),
90 crossover_p: Probability::new(0.5),
91 tournament_size: 2,
92 elitism_k: 1,
93 }
94 }
95}
96
97impl Validate for BinaryGaConfig {
98 fn validate(&self) -> Result<(), ConfigError> {
99 const C: &str = "BinaryGaConfig";
100 config::at_least(C, "pop_size", self.pop_size, 1)?;
101 config::nonzero(C, "genome_dim", self.genome_dim)?;
102 config::at_least(C, "tournament_size", self.tournament_size, 1)?;
106 if self.tournament_size > self.pop_size {
107 return Err(ConfigError {
108 config: C,
109 field: "tournament_size",
110 kind: ConstraintKind::Custom("tournament_size must not exceed pop_size"),
111 });
112 }
113 if self.elitism_k > self.pop_size {
114 return Err(ConfigError {
115 config: C,
116 field: "elitism_k",
117 kind: ConstraintKind::Custom("elitism_k must not exceed pop_size"),
118 });
119 }
120 Ok(())
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct BinaryGaState<B: Backend> {
127 pub population: Tensor<B, 2, Int>,
129 pub fitness: Vec<f32>,
132 pub best_genome: Option<Tensor<B, 2, Int>>,
134 pub best_fitness: f32,
136 pub generation: usize,
138}
139
140#[derive(Debug, Clone, Copy, Default)]
153pub struct BinaryGeneticAlgorithm<B: Backend> {
154 _backend: PhantomData<fn() -> B>,
155}
156
157impl<B: Backend> BinaryGeneticAlgorithm<B> {
158 #[must_use]
160 pub fn new() -> Self {
161 Self {
162 _backend: PhantomData,
163 }
164 }
165
166 fn sample_initial_population(
167 params: &BinaryGaConfig,
168 rng: &mut dyn Rng,
169 device: &<B as burn::tensor::backend::BackendTypes>::Device,
170 ) -> Tensor<B, 2, Int> {
171 let pop = params.pop_size;
176 let genome_dim = params.genome_dim;
177 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
178 let mut rows = Vec::with_capacity(pop * genome_dim);
179 for _ in 0..pop * genome_dim {
180 rows.push(stream.random::<f32>());
181 }
182 let u = Tensor::<B, 2>::from_data(TensorData::new(rows, [pop, genome_dim]), device);
183 u.lower_elem(0.5).int()
184 }
185}
186
187impl<B: Backend> Strategy<B> for BinaryGeneticAlgorithm<B>
188where
189 B::Device: Clone,
190{
191 type Params = BinaryGaConfig;
192 type State = BinaryGaState<B>;
193 type Genome = Tensor<B, 2, Int>;
194
195 fn init(
203 &self,
204 params: &BinaryGaConfig,
205 rng: &mut dyn Rng,
206 device: &<B as burn::tensor::backend::BackendTypes>::Device,
207 ) -> BinaryGaState<B> {
208 debug_assert!(
209 params.validate().is_ok(),
210 "invalid BinaryGaConfig reached init: {params:?}"
211 );
212 BinaryGaState {
213 population: Self::sample_initial_population(params, rng, device),
214 fitness: Vec::new(),
215 best_genome: None,
216 best_fitness: f32::NEG_INFINITY,
217 generation: 0,
218 }
219 }
220
221 fn ask(
251 &self,
252 params: &BinaryGaConfig,
253 state: &BinaryGaState<B>,
254 rng: &mut dyn Rng,
255 device: &<B as burn::tensor::backend::BackendTypes>::Device,
256 ) -> (Tensor<B, 2, Int>, BinaryGaState<B>) {
257 if state.fitness.is_empty() {
258 return (state.population.clone(), state.clone());
259 }
260
261 let mut selection_rng = seed_stream(
262 rng.next_u64(),
263 state.generation as u64,
264 SeedPurpose::Selection,
265 );
266 let mut crossover_rng = seed_stream(
267 rng.next_u64(),
268 state.generation as u64,
269 SeedPurpose::Crossover,
270 );
271 let mut mutation_rng = seed_stream(
272 rng.next_u64(),
273 state.generation as u64,
274 SeedPurpose::Mutation,
275 );
276
277 let idx_a = tournament_indices_host(
278 &state.fitness,
279 params.tournament_size,
280 params.pop_size,
281 &mut selection_rng,
282 );
283 let idx_b = tournament_indices_host(
284 &state.fitness,
285 params.tournament_size,
286 params.pop_size,
287 &mut selection_rng,
288 );
289 let parents_a = state.population.clone().select(
290 0,
291 Tensor::<B, 1, Int>::from_data(TensorData::new(idx_a, [params.pop_size]), device),
292 );
293 let parents_b = state.population.clone().select(
294 0,
295 Tensor::<B, 1, Int>::from_data(TensorData::new(idx_b, [params.pop_size]), device),
296 );
297
298 let offspring = binary_uniform_crossover(
299 parents_a,
300 parents_b,
301 params.crossover_p,
302 &mut crossover_rng,
303 device,
304 );
305
306 let offspring =
307 bit_flip_mutation(offspring, params.mutation_rate, &mut mutation_rng, device);
308
309 (offspring, state.clone())
310 }
311
312 fn tell(
339 &self,
340 params: &BinaryGaConfig,
341 offspring: Tensor<B, 2, Int>,
342 fitness: Tensor<B, 1>,
343 mut state: BinaryGaState<B>,
344 _rng: &mut dyn Rng,
345 ) -> (BinaryGaState<B>, StrategyMetrics) {
346 let fitness_host = fitness
347 .into_data()
348 .into_vec::<f32>()
349 .expect("fitness tensor must be readable as f32");
350 let device = offspring.device();
351
352 if state.fitness.is_empty() {
354 state.fitness.clone_from(&fitness_host);
355 state.generation += 1;
356 update_best(&mut state, &offspring, &fitness_host);
357 let m = StrategyMetrics::from_host_fitness(
358 state.generation,
359 &fitness_host,
360 state.best_fitness,
361 );
362 state.best_fitness = m.best_fitness_ever();
363 state.population = offspring;
364 return (state, m);
365 }
366
367 let pop_size = params.pop_size;
369 let k = params.elitism_k.min(pop_size);
370
371 let elite_idx = truncation_indices_host(&state.fitness, k);
372 let elites = state.population.clone().select(
373 0,
374 Tensor::<B, 1, Int>::from_data(TensorData::new(elite_idx.clone(), [k]), &device),
375 );
376 let n_off_keep = pop_size - k;
377 let off_keep_idx = truncation_indices_host(&fitness_host, n_off_keep);
378 let kept_off = offspring.clone().select(
379 0,
380 Tensor::<B, 1, Int>::from_data(
381 TensorData::new(off_keep_idx.clone(), [n_off_keep]),
382 &device,
383 ),
384 );
385 let next_pop = Tensor::cat(vec![elites, kept_off], 0);
386 let mut next_fit = Vec::with_capacity(pop_size);
387 for i in elite_idx {
388 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
389 next_fit.push(state.fitness[i as usize]);
390 }
391 for i in off_keep_idx {
392 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
393 next_fit.push(fitness_host[i as usize]);
394 }
395
396 update_best(&mut state, &next_pop, &next_fit);
397 state.population = next_pop;
398 state.fitness.clone_from(&next_fit);
399 state.generation += 1;
400 let m = StrategyMetrics::from_host_fitness(state.generation, &next_fit, state.best_fitness);
401 state.best_fitness = m.best_fitness_ever();
402 (state, m)
403 }
404
405 fn best(&self, state: &BinaryGaState<B>) -> Option<(Tensor<B, 2, Int>, f32)> {
410 state
411 .best_genome
412 .as_ref()
413 .map(|g| (g.clone(), state.best_fitness))
414 }
415}
416
417fn update_best<B: Backend>(state: &mut BinaryGaState<B>, pop: &Tensor<B, 2, Int>, fitness: &[f32]) {
418 if fitness.is_empty() {
419 return;
420 }
421 let mut best_idx = 0usize;
422 let mut best_f = fitness[0];
423 for (i, &f) in fitness.iter().enumerate().skip(1) {
424 if f > best_f {
425 best_f = f;
426 best_idx = i;
427 }
428 }
429 if best_f > state.best_fitness {
430 let device = pop.device();
431 #[allow(clippy::cast_possible_wrap)]
432 let idx =
433 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![best_idx as i64], [1]), &device);
434 state.best_genome = Some(pop.clone().select(0, idx));
435 state.best_fitness = best_f;
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::fitness::BatchFitnessFn;
443 use crate::strategy::EvolutionaryHarness;
444 use burn::backend::Flex;
445 type TestBackend = Flex;
446
447 #[test]
448 fn default_config_validates() {
449 assert!(BinaryGaConfig::default_for(32, 16).validate().is_ok());
450 }
451
452 #[test]
453 fn rejects_elitism_larger_than_pop() {
454 let mut cfg = BinaryGaConfig::default_for(8, 16);
455 cfg.elitism_k = 16;
456 assert_eq!(cfg.validate().unwrap_err().field, "elitism_k");
457 }
458
459 struct OneMax {
462 dim: usize,
463 }
464
465 impl<B: Backend> BatchFitnessFn<B, Tensor<B, 2, Int>> for OneMax {
466 fn evaluate_batch(
467 &mut self,
468 population: &Tensor<B, 2, Int>,
469 device: &<B as burn::tensor::backend::BackendTypes>::Device,
470 ) -> Tensor<B, 1> {
471 let dims = population.dims();
472 let pop_size = dims[0];
473 let data = population
474 .clone()
475 .into_data()
476 .into_vec::<i32>()
477 .expect("genome host-read of a tensor this test just built");
478 let mut fitness = Vec::with_capacity(pop_size);
479 for row in 0..pop_size {
480 let mut ones = 0_u32;
481 for col in 0..self.dim {
482 if data[row * self.dim + col] != 0 {
483 ones += 1;
484 }
485 }
486 #[allow(clippy::cast_precision_loss)]
487 fitness.push(ones as f32);
488 }
489 Tensor::<B, 1>::from_data(TensorData::new(fitness, [pop_size]), device)
490 }
491
492 fn sense(&self) -> rlevo_core::objective::ObjectiveSense {
493 rlevo_core::objective::ObjectiveSense::Maximize
494 }
495 }
496
497 #[test]
498 fn binary_ga_solves_onemax() {
499 let device = Default::default();
500 let dim = 16;
501 let params = BinaryGaConfig::default_for(32, dim);
502 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
503 BinaryGeneticAlgorithm::<TestBackend>::new(),
504 params,
505 OneMax { dim },
506 7,
507 device,
508 200,
509 )
510 .expect("valid params");
511 harness.reset();
512 loop {
513 if harness.step(()).done {
514 break;
515 }
516 }
517 let best = harness.latest_metrics().unwrap().best_fitness_ever();
518 #[allow(clippy::cast_precision_loss)]
520 let optimum = dim as f32;
521 approx::assert_relative_eq!(best, optimum, epsilon = 1e-6);
522 }
523
524 fn ask_over_uniform_population(rate: f32, bit: i32) -> Vec<i32> {
530 use rand::SeedableRng;
531 use rand::rngs::StdRng;
532
533 let device = Default::default();
534 let (pop, dim) = (4usize, 8usize);
535 let mut params = BinaryGaConfig::default_for(pop, dim);
536 params.mutation_rate = Probability::new(rate);
537 let strategy = BinaryGeneticAlgorithm::<TestBackend>::new();
538
539 let population = Tensor::<TestBackend, 2, Int>::from_data(
540 TensorData::new(vec![bit; pop * dim], [pop, dim]),
541 &device,
542 );
543 let state = BinaryGaState {
546 population,
547 fitness: vec![1.0; pop],
548 best_genome: None,
549 best_fitness: f32::NEG_INFINITY,
550 generation: 1,
551 };
552 let mut rng = StdRng::seed_from_u64(0);
553 let (offspring, _) = strategy.ask(¶ms, &state, &mut rng, &device);
554 offspring
555 .into_data()
556 .into_vec::<i32>()
557 .expect("offspring host-read of a tensor this test just built")
558 }
559
560 #[test]
564 fn mutation_rate_zero_flips_no_bits() {
565 let bits = ask_over_uniform_population(0.0, 0);
566 assert!(
567 bits.iter().all(|&b| b == 0),
568 "rate 0.0 must leave every offspring bit at the parent value, got {bits:?}"
569 );
570 }
571
572 #[test]
575 fn mutation_rate_one_flips_every_bit() {
576 let bits = ask_over_uniform_population(1.0, 0);
577 assert!(
578 bits.iter().all(|&b| b == 1),
579 "rate 1.0 must flip every 0→1, got {bits:?}"
580 );
581 }
582
583 #[test]
588 fn elitism_k_equals_pop_minus_one_keeps_top_parents_and_one_offspring() {
589 use rand::SeedableRng;
590 use rand::rngs::StdRng;
591
592 let device = Default::default();
593 let (pop, dim) = (4usize, 4usize);
594 let mut params = BinaryGaConfig::default_for(pop, dim);
595 params.elitism_k = pop - 1; let strategy = BinaryGeneticAlgorithm::<TestBackend>::new();
597
598 let parent_pop = Tensor::<TestBackend, 2, Int>::from_data(
600 TensorData::new(vec![0i32; pop * dim], [pop, dim]),
601 &device,
602 );
603 let state = BinaryGaState {
604 population: parent_pop,
605 fitness: vec![1.0, 2.0, 3.0, 4.0],
606 best_genome: None,
607 best_fitness: 4.0,
608 generation: 1,
609 };
610
611 let offspring = Tensor::<TestBackend, 2, Int>::from_data(
613 TensorData::new(vec![1i32; pop * dim], [pop, dim]),
614 &device,
615 );
616 let off_fitness = Tensor::<TestBackend, 1>::from_data(
617 TensorData::new(vec![10.0f32, 0.0, 0.0, 0.0], [pop]),
618 &device,
619 );
620
621 let mut rng = StdRng::seed_from_u64(0);
622 let (next, m) = strategy.tell(¶ms, offspring, off_fitness, state, &mut rng);
623
624 assert_eq!(next.population.dims(), [pop, dim]);
627 assert_eq!(next.fitness, vec![4.0, 3.0, 2.0, 10.0]);
628 approx::assert_relative_eq!(m.best_fitness_ever(), 10.0, epsilon = 1e-6);
629 }
630}