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};
50
51#[derive(Debug, Clone)]
53pub struct BinaryGaConfig {
54 pub pop_size: usize,
56 pub genome_dim: usize,
58 pub mutation_rate: f32,
60 pub crossover_p: f32,
62 pub tournament_size: usize,
64 pub elitism_k: usize,
66}
67
68impl BinaryGaConfig {
69 #[must_use]
74 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
75 Self {
76 pop_size,
77 genome_dim,
78 #[allow(clippy::cast_precision_loss)]
79 mutation_rate: 1.0 / genome_dim as f32,
80 crossover_p: 0.5,
81 tournament_size: 2,
82 elitism_k: 1,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct BinaryGaState<B: Backend> {
90 pub population: Tensor<B, 2, Int>,
92 pub fitness: Vec<f32>,
95 pub best_genome: Option<Tensor<B, 2, Int>>,
97 pub best_fitness: f32,
99 pub generation: usize,
101}
102
103#[derive(Debug, Clone, Copy, Default)]
116pub struct BinaryGeneticAlgorithm<B: Backend> {
117 _backend: PhantomData<fn() -> B>,
118}
119
120impl<B: Backend> BinaryGeneticAlgorithm<B> {
121 #[must_use]
123 pub fn new() -> Self {
124 Self {
125 _backend: PhantomData,
126 }
127 }
128
129 fn sample_initial_population(
130 params: &BinaryGaConfig,
131 rng: &mut dyn Rng,
132 device: &<B as burn::tensor::backend::BackendTypes>::Device,
133 ) -> Tensor<B, 2, Int> {
134 let pop = params.pop_size;
139 let genome_dim = params.genome_dim;
140 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
141 let mut rows = Vec::with_capacity(pop * genome_dim);
142 for _ in 0..pop * genome_dim {
143 rows.push(stream.random::<f32>());
144 }
145 let u = Tensor::<B, 2>::from_data(TensorData::new(rows, [pop, genome_dim]), device);
146 u.lower_elem(0.5).int()
147 }
148}
149
150impl<B: Backend> Strategy<B> for BinaryGeneticAlgorithm<B>
151where
152 B::Device: Clone,
153{
154 type Params = BinaryGaConfig;
155 type State = BinaryGaState<B>;
156 type Genome = Tensor<B, 2, Int>;
157
158 fn init(
165 &self,
166 params: &BinaryGaConfig,
167 rng: &mut dyn Rng,
168 device: &<B as burn::tensor::backend::BackendTypes>::Device,
169 ) -> BinaryGaState<B> {
170 BinaryGaState {
171 population: Self::sample_initial_population(params, rng, device),
172 fitness: Vec::new(),
173 best_genome: None,
174 best_fitness: f32::INFINITY,
175 generation: 0,
176 }
177 }
178
179 fn ask(
197 &self,
198 params: &BinaryGaConfig,
199 state: &BinaryGaState<B>,
200 rng: &mut dyn Rng,
201 device: &<B as burn::tensor::backend::BackendTypes>::Device,
202 ) -> (Tensor<B, 2, Int>, BinaryGaState<B>) {
203 if state.fitness.is_empty() {
204 return (state.population.clone(), state.clone());
205 }
206
207 let mut selection_rng = seed_stream(
208 rng.next_u64(),
209 state.generation as u64,
210 SeedPurpose::Selection,
211 );
212 let mut crossover_rng = seed_stream(
213 rng.next_u64(),
214 state.generation as u64,
215 SeedPurpose::Crossover,
216 );
217 let mut mutation_rng = seed_stream(
218 rng.next_u64(),
219 state.generation as u64,
220 SeedPurpose::Mutation,
221 );
222
223 let idx_a = tournament_indices_host(
224 &state.fitness,
225 params.tournament_size,
226 params.pop_size,
227 &mut selection_rng,
228 );
229 let idx_b = tournament_indices_host(
230 &state.fitness,
231 params.tournament_size,
232 params.pop_size,
233 &mut selection_rng,
234 );
235 let parents_a = state.population.clone().select(
236 0,
237 Tensor::<B, 1, Int>::from_data(TensorData::new(idx_a, [params.pop_size]), device),
238 );
239 let parents_b = state.population.clone().select(
240 0,
241 Tensor::<B, 1, Int>::from_data(TensorData::new(idx_b, [params.pop_size]), device),
242 );
243
244 let offspring = binary_uniform_crossover(
245 parents_a,
246 parents_b,
247 params.crossover_p,
248 &mut crossover_rng,
249 device,
250 );
251
252 let offspring = bit_flip_mutation(offspring, params.mutation_rate, &mut mutation_rng, device);
253
254 (offspring, state.clone())
255 }
256
257 fn tell(
271 &self,
272 params: &BinaryGaConfig,
273 offspring: Tensor<B, 2, Int>,
274 fitness: Tensor<B, 1>,
275 mut state: BinaryGaState<B>,
276 _rng: &mut dyn Rng,
277 ) -> (BinaryGaState<B>, StrategyMetrics) {
278 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
279 let device = offspring.device();
280
281 if state.fitness.is_empty() {
283 state.fitness.clone_from(&fitness_host);
284 state.generation += 1;
285 update_best(&mut state, &offspring, &fitness_host);
286 let m = StrategyMetrics::from_host_fitness(
287 state.generation,
288 &fitness_host,
289 state.best_fitness,
290 );
291 state.best_fitness = m.best_fitness_ever;
292 state.population = offspring;
293 return (state, m);
294 }
295
296 let pop_size = params.pop_size;
298 let k = params.elitism_k.min(pop_size);
299
300 let elite_idx = truncation_indices_host(&state.fitness, k);
301 let elites = state.population.clone().select(
302 0,
303 Tensor::<B, 1, Int>::from_data(TensorData::new(elite_idx.clone(), [k]), &device),
304 );
305 let n_off_keep = pop_size - k;
306 let off_keep_idx = truncation_indices_host(&fitness_host, n_off_keep);
307 let kept_off = offspring.clone().select(
308 0,
309 Tensor::<B, 1, Int>::from_data(
310 TensorData::new(off_keep_idx.clone(), [n_off_keep]),
311 &device,
312 ),
313 );
314 let next_pop = Tensor::cat(vec![elites, kept_off], 0);
315 let mut next_fit = Vec::with_capacity(pop_size);
316 for i in elite_idx {
317 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
318 next_fit.push(state.fitness[i as usize]);
319 }
320 for i in off_keep_idx {
321 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
322 next_fit.push(fitness_host[i as usize]);
323 }
324
325 update_best(&mut state, &next_pop, &next_fit);
326 state.population = next_pop;
327 state.fitness.clone_from(&next_fit);
328 state.generation += 1;
329 let m = StrategyMetrics::from_host_fitness(state.generation, &next_fit, state.best_fitness);
330 state.best_fitness = m.best_fitness_ever;
331 (state, m)
332 }
333
334 fn best(&self, state: &BinaryGaState<B>) -> Option<(Tensor<B, 2, Int>, f32)> {
339 state
340 .best_genome
341 .as_ref()
342 .map(|g| (g.clone(), state.best_fitness))
343 }
344}
345
346fn update_best<B: Backend>(state: &mut BinaryGaState<B>, pop: &Tensor<B, 2, Int>, fitness: &[f32]) {
347 if fitness.is_empty() {
348 return;
349 }
350 let mut best_idx = 0usize;
351 let mut best_f = fitness[0];
352 for (i, &f) in fitness.iter().enumerate().skip(1) {
353 if f < best_f {
354 best_f = f;
355 best_idx = i;
356 }
357 }
358 if best_f < state.best_fitness {
359 let device = pop.device();
360 #[allow(clippy::cast_possible_wrap)]
361 let idx =
362 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![best_idx as i64], [1]), &device);
363 state.best_genome = Some(pop.clone().select(0, idx));
364 state.best_fitness = best_f;
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use crate::fitness::BatchFitnessFn;
372 use crate::strategy::EvolutionaryHarness;
373 use burn::backend::Flex;
374 type TestBackend = Flex;
375
376 struct OneMaxCost {
378 dim: usize,
379 }
380
381 impl<B: Backend> BatchFitnessFn<B, Tensor<B, 2, Int>> for OneMaxCost {
382 fn evaluate_batch(
383 &mut self,
384 population: &Tensor<B, 2, Int>,
385 device: &<B as burn::tensor::backend::BackendTypes>::Device,
386 ) -> Tensor<B, 1> {
387 let dims = population.dims();
388 let pop_size = dims[0];
389 let data = population
390 .clone()
391 .into_data()
392 .into_vec::<i32>()
393 .unwrap_or_default();
394 let mut fitness = Vec::with_capacity(pop_size);
395 for row in 0..pop_size {
396 let mut ones = 0_u32;
397 for col in 0..self.dim {
398 if data[row * self.dim + col] != 0 {
399 ones += 1;
400 }
401 }
402 #[allow(clippy::cast_precision_loss)]
403 let cost = (self.dim as f32) - (ones as f32);
404 fitness.push(cost);
405 }
406 Tensor::<B, 1>::from_data(TensorData::new(fitness, [pop_size]), device)
407 }
408 }
409
410 #[test]
411 fn binary_ga_solves_onemax() {
412 let device = Default::default();
413 let dim = 16;
414 let params = BinaryGaConfig::default_for(32, dim);
415 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
416 BinaryGeneticAlgorithm::<TestBackend>::new(),
417 params,
418 OneMaxCost { dim },
419 7,
420 device,
421 200,
422 );
423 harness.reset();
424 loop {
425 if harness.step(()).done {
426 break;
427 }
428 }
429 let best = harness.latest_metrics().unwrap().best_fitness_ever;
430 approx::assert_relative_eq!(best, 0.0, epsilon = 1e-6);
432 }
433}