1use std::marker::PhantomData;
15
16use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
17use rand::Rng;
18
19use crate::ops::crossover::binary_uniform_crossover;
20use crate::ops::mutation::bit_flip_mutation;
21use crate::ops::selection::{tournament_indices_host, truncation_indices_host};
22use crate::rng::{SeedPurpose, seed_stream};
23use crate::strategy::{Strategy, StrategyMetrics};
24
25#[derive(Debug, Clone)]
27pub struct BinaryGaConfig {
28 pub pop_size: usize,
30 pub genome_dim: usize,
32 pub mutation_rate: f32,
34 pub crossover_p: f32,
36 pub tournament_size: usize,
38 pub elitism_k: usize,
40}
41
42impl BinaryGaConfig {
43 #[must_use]
48 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
49 Self {
50 pop_size,
51 genome_dim,
52 #[allow(clippy::cast_precision_loss)]
53 mutation_rate: 1.0 / genome_dim as f32,
54 crossover_p: 0.5,
55 tournament_size: 2,
56 elitism_k: 1,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct BinaryGaState<B: Backend> {
64 pub population: Tensor<B, 2, Int>,
66 pub fitness: Vec<f32>,
69 pub best_genome: Option<Tensor<B, 2, Int>>,
71 pub best_fitness: f32,
73 pub generation: usize,
75}
76
77#[derive(Debug, Clone, Copy, Default)]
90pub struct BinaryGeneticAlgorithm<B: Backend> {
91 _backend: PhantomData<fn() -> B>,
92}
93
94impl<B: Backend> BinaryGeneticAlgorithm<B> {
95 #[must_use]
97 pub fn new() -> Self {
98 Self {
99 _backend: PhantomData,
100 }
101 }
102
103 fn sample_initial_population(
104 params: &BinaryGaConfig,
105 rng: &mut dyn Rng,
106 device: &B::Device,
107 ) -> Tensor<B, 2, Int> {
108 B::seed(device, rng.next_u64());
109 let u = Tensor::<B, 2>::random(
110 [params.pop_size, params.genome_dim],
111 burn::tensor::Distribution::Uniform(0.0, 1.0),
112 device,
113 );
114 u.lower_elem(0.5).int()
115 }
116}
117
118impl<B: Backend> Strategy<B> for BinaryGeneticAlgorithm<B>
119where
120 B::Device: Clone,
121{
122 type Params = BinaryGaConfig;
123 type State = BinaryGaState<B>;
124 type Genome = Tensor<B, 2, Int>;
125
126 fn init(
127 &self,
128 params: &BinaryGaConfig,
129 rng: &mut dyn Rng,
130 device: &B::Device,
131 ) -> BinaryGaState<B> {
132 BinaryGaState {
133 population: Self::sample_initial_population(params, rng, device),
134 fitness: Vec::new(),
135 best_genome: None,
136 best_fitness: f32::INFINITY,
137 generation: 0,
138 }
139 }
140
141 fn ask(
142 &self,
143 params: &BinaryGaConfig,
144 state: &BinaryGaState<B>,
145 rng: &mut dyn Rng,
146 device: &B::Device,
147 ) -> (Tensor<B, 2, Int>, BinaryGaState<B>) {
148 if state.fitness.is_empty() {
149 return (state.population.clone(), state.clone());
150 }
151
152 let mut selection_rng = seed_stream(
153 rng.next_u64(),
154 state.generation as u64,
155 SeedPurpose::Selection,
156 );
157 let mut crossover_rng = seed_stream(
158 rng.next_u64(),
159 state.generation as u64,
160 SeedPurpose::Crossover,
161 );
162 let mut mutation_rng = seed_stream(
163 rng.next_u64(),
164 state.generation as u64,
165 SeedPurpose::Mutation,
166 );
167
168 let idx_a = tournament_indices_host(
169 &state.fitness,
170 params.tournament_size,
171 params.pop_size,
172 &mut selection_rng,
173 );
174 let idx_b = tournament_indices_host(
175 &state.fitness,
176 params.tournament_size,
177 params.pop_size,
178 &mut selection_rng,
179 );
180 let parents_a = state.population.clone().select(
181 0,
182 Tensor::<B, 1, Int>::from_data(TensorData::new(idx_a, [params.pop_size]), device),
183 );
184 let parents_b = state.population.clone().select(
185 0,
186 Tensor::<B, 1, Int>::from_data(TensorData::new(idx_b, [params.pop_size]), device),
187 );
188
189 B::seed(device, crossover_rng.next_u64());
190 let offspring = binary_uniform_crossover(parents_a, parents_b, params.crossover_p, device);
191
192 B::seed(device, mutation_rng.next_u64());
193 let offspring = bit_flip_mutation(offspring, params.mutation_rate, device);
194
195 (offspring, state.clone())
196 }
197
198 fn tell(
199 &self,
200 params: &BinaryGaConfig,
201 offspring: Tensor<B, 2, Int>,
202 fitness: Tensor<B, 1>,
203 mut state: BinaryGaState<B>,
204 _rng: &mut dyn Rng,
205 ) -> (BinaryGaState<B>, StrategyMetrics) {
206 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
207 let device = offspring.device();
208
209 if state.fitness.is_empty() {
211 state.fitness.clone_from(&fitness_host);
212 state.generation += 1;
213 update_best(&mut state, &offspring, &fitness_host);
214 let m = StrategyMetrics::from_host_fitness(
215 state.generation,
216 &fitness_host,
217 state.best_fitness,
218 );
219 state.best_fitness = m.best_fitness_ever;
220 state.population = offspring;
221 return (state, m);
222 }
223
224 let pop_size = params.pop_size;
226 let k = params.elitism_k.min(pop_size);
227
228 let elite_idx = truncation_indices_host(&state.fitness, k);
229 let elites = state.population.clone().select(
230 0,
231 Tensor::<B, 1, Int>::from_data(TensorData::new(elite_idx.clone(), [k]), &device),
232 );
233 let n_off_keep = pop_size - k;
234 let off_keep_idx = truncation_indices_host(&fitness_host, n_off_keep);
235 let kept_off = offspring.clone().select(
236 0,
237 Tensor::<B, 1, Int>::from_data(
238 TensorData::new(off_keep_idx.clone(), [n_off_keep]),
239 &device,
240 ),
241 );
242 let next_pop = Tensor::cat(vec![elites, kept_off], 0);
243 let mut next_fit = Vec::with_capacity(pop_size);
244 for i in elite_idx {
245 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
246 next_fit.push(state.fitness[i as usize]);
247 }
248 for i in off_keep_idx {
249 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
250 next_fit.push(fitness_host[i as usize]);
251 }
252
253 update_best(&mut state, &next_pop, &next_fit);
254 state.population = next_pop;
255 state.fitness.clone_from(&next_fit);
256 state.generation += 1;
257 let m = StrategyMetrics::from_host_fitness(state.generation, &next_fit, state.best_fitness);
258 state.best_fitness = m.best_fitness_ever;
259 (state, m)
260 }
261
262 fn best(&self, state: &BinaryGaState<B>) -> Option<(Tensor<B, 2, Int>, f32)> {
263 state
264 .best_genome
265 .as_ref()
266 .map(|g| (g.clone(), state.best_fitness))
267 }
268}
269
270fn update_best<B: Backend>(state: &mut BinaryGaState<B>, pop: &Tensor<B, 2, Int>, fitness: &[f32]) {
271 if fitness.is_empty() {
272 return;
273 }
274 let mut best_idx = 0usize;
275 let mut best_f = fitness[0];
276 for (i, &f) in fitness.iter().enumerate().skip(1) {
277 if f < best_f {
278 best_f = f;
279 best_idx = i;
280 }
281 }
282 if best_f < state.best_fitness {
283 let device = pop.device();
284 #[allow(clippy::cast_possible_wrap)]
285 let idx =
286 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![best_idx as i64], [1]), &device);
287 state.best_genome = Some(pop.clone().select(0, idx));
288 state.best_fitness = best_f;
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::fitness::BatchFitnessFn;
296 use crate::strategy::EvolutionaryHarness;
297 use burn::backend::NdArray;
298 type TestBackend = NdArray;
299
300 struct OneMaxCost {
302 dim: usize,
303 }
304
305 impl<B: Backend> BatchFitnessFn<B, Tensor<B, 2, Int>> for OneMaxCost {
306 fn evaluate_batch(
307 &mut self,
308 population: &Tensor<B, 2, Int>,
309 device: &B::Device,
310 ) -> Tensor<B, 1> {
311 let dims = population.shape().dims;
312 let pop_size = dims[0];
313 let data = population
314 .clone()
315 .into_data()
316 .into_vec::<i64>()
317 .unwrap_or_default();
318 let mut fitness = Vec::with_capacity(pop_size);
319 for row in 0..pop_size {
320 let mut ones = 0_u32;
321 for col in 0..self.dim {
322 if data[row * self.dim + col] != 0 {
323 ones += 1;
324 }
325 }
326 #[allow(clippy::cast_precision_loss)]
327 let cost = (self.dim as f32) - (ones as f32);
328 fitness.push(cost);
329 }
330 Tensor::<B, 1>::from_data(TensorData::new(fitness, [pop_size]), device)
331 }
332 }
333
334 #[test]
335 fn binary_ga_solves_onemax() {
336 let device = Default::default();
337 let dim = 16;
338 let params = BinaryGaConfig::default_for(32, dim);
339 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
340 BinaryGeneticAlgorithm::<TestBackend>::new(),
341 params,
342 OneMaxCost { dim },
343 7,
344 device,
345 200,
346 );
347 harness.reset();
348 loop {
349 if harness.step(()).done {
350 break;
351 }
352 }
353 let best = harness.latest_metrics().unwrap().best_fitness_ever;
354 approx::assert_relative_eq!(best, 0.0, epsilon = 1e-6);
356 }
357}