rlevo_evolution/algorithms/metaheuristic/
bat.rs1use std::marker::PhantomData;
29
30use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
31use rand::Rng;
32use rand::RngExt;
33
34use crate::rng::{SeedPurpose, seed_stream};
35use crate::strategy::{Strategy, StrategyMetrics};
36
37#[derive(Debug, Clone)]
39pub struct BatConfig {
40 pub pop_size: usize,
42 pub genome_dim: usize,
44 pub bounds: (f32, f32),
46 pub f_min: f32,
48 pub f_max: f32,
50 pub a0: f32,
52 pub r0: f32,
54 pub alpha: f32,
56 pub gamma: f32,
58}
59
60impl BatConfig {
61 #[must_use]
63 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
64 Self {
65 pop_size,
66 genome_dim,
67 bounds: (-5.12, 5.12),
68 f_min: 0.0,
69 f_max: 2.0,
70 a0: 1.0,
71 r0: 0.5,
72 alpha: 0.9,
73 gamma: 0.9,
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct BatState<B: Backend> {
81 pub positions: Tensor<B, 2>,
83 pub velocities: Tensor<B, 2>,
85 pub loudness: Vec<f32>,
87 pub pulse_rate: Vec<f32>,
89 pub fitness: Vec<f32>,
91 pub best_genome: Option<Tensor<B, 2>>,
93 pub best_fitness: f32,
95 pub generation: usize,
97 pub pending_accept: Vec<bool>,
101}
102
103#[derive(Debug, Clone, Copy, Default)]
116pub struct BatAlgorithm<B: Backend> {
117 _backend: PhantomData<fn() -> B>,
118}
119
120impl<B: Backend> BatAlgorithm<B> {
121 #[must_use]
123 pub fn new() -> Self {
124 Self {
125 _backend: PhantomData,
126 }
127 }
128}
129
130impl<B: Backend> Strategy<B> for BatAlgorithm<B>
131where
132 B::Device: Clone,
133{
134 type Params = BatConfig;
135 type State = BatState<B>;
136 type Genome = Tensor<B, 2>;
137
138 fn init(&self, params: &BatConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> BatState<B> {
148 let (lo, hi) = params.bounds;
149 let pop = params.pop_size;
157 let genome_dim = params.genome_dim;
158 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
159 let mut position_rows = Vec::with_capacity(pop * genome_dim);
160 for _ in 0..pop * genome_dim {
161 position_rows.push(lo + (hi - lo) * stream.random::<f32>());
162 }
163 let positions =
164 Tensor::<B, 2>::from_data(TensorData::new(position_rows, [pop, genome_dim]), device);
165 let velocities = Tensor::<B, 2>::zeros([params.pop_size, params.genome_dim], device);
166 BatState {
167 positions,
168 velocities,
169 loudness: vec![params.a0; params.pop_size],
170 pulse_rate: vec![params.r0; params.pop_size],
171 fitness: Vec::new(),
172 best_genome: None,
173 best_fitness: f32::INFINITY,
174 generation: 0,
175 pending_accept: Vec::new(),
176 }
177 }
178
179 fn ask(
204 &self,
205 params: &BatConfig,
206 state: &BatState<B>,
207 rng: &mut dyn Rng,
208 device: &<B as burn::tensor::backend::BackendTypes>::Device,
209 ) -> (Tensor<B, 2>, BatState<B>) {
210 if state.fitness.is_empty() {
211 return (state.positions.clone(), state.clone());
214 }
215
216 let pop = params.pop_size;
217 let genome_dim = params.genome_dim;
218 let (lo, hi) = params.bounds;
219
220 let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
225
226 let mut betas = Vec::with_capacity(pop);
227 let mut use_local = Vec::with_capacity(pop);
228 let mut accept_draw = Vec::with_capacity(pop);
229 let mut epsilon_rows = Vec::with_capacity(pop * genome_dim);
230 for i in 0..pop {
231 betas.push(stream.random::<f32>());
232 use_local.push(stream.random::<f32>() > state.pulse_rate[i]);
233 accept_draw.push(stream.random::<f32>());
234 for _ in 0..genome_dim {
235 epsilon_rows.push(2.0 * stream.random::<f32>() - 1.0);
236 }
237 }
238
239 let mean_loudness: f32 = {
242 let s: f32 = state.loudness.iter().sum();
243 #[allow(clippy::cast_precision_loss)]
244 {
245 s / pop as f32
246 }
247 };
248
249 let best = state
250 .best_genome
251 .as_ref()
252 .expect("best populated after first tell")
253 .clone()
254 .expand([pop, genome_dim]);
255
256 let f_vec: Vec<f32> = betas
258 .iter()
259 .map(|b| params.f_min + (params.f_max - params.f_min) * b)
260 .collect();
261 let f_mat = Tensor::<B, 1>::from_data(TensorData::new(f_vec, [pop]), device)
262 .unsqueeze_dim::<2>(1)
263 .expand([pop, genome_dim]);
264
265 let new_velocities =
266 state.velocities.clone() + (state.positions.clone() - best.clone()).mul(f_mat);
267 let global_move = state.positions.clone() + new_velocities.clone();
268 let eps =
270 Tensor::<B, 2>::from_data(TensorData::new(epsilon_rows, [pop, genome_dim]), device);
271 let local_move = best + eps.mul_scalar(mean_loudness);
272
273 #[allow(clippy::cast_possible_wrap)]
274 let mask = Tensor::<B, 1, Int>::from_data(
275 TensorData::new(
276 use_local.iter().map(|&b| i64::from(b)).collect::<Vec<_>>(),
277 [pop],
278 ),
279 device,
280 )
281 .equal_elem(1)
282 .unsqueeze_dim::<2>(1)
283 .expand([pop, genome_dim]);
284 let candidates = global_move.mask_where(mask, local_move).clamp(lo, hi);
285
286 let mut next = state.clone();
288 next.velocities = new_velocities;
289 next.pending_accept = accept_draw
290 .iter()
291 .zip(state.loudness.iter())
292 .map(|(&draw, &a)| draw < a)
293 .collect();
294 (candidates, next)
295 }
296
297 fn tell(
310 &self,
311 params: &BatConfig,
312 candidates: Tensor<B, 2>,
313 fitness: Tensor<B, 1>,
314 mut state: BatState<B>,
315 _rng: &mut dyn Rng,
316 ) -> (BatState<B>, StrategyMetrics) {
317 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
318 let device = candidates.device();
319 let pop = params.pop_size;
320 let genome_dim = params.genome_dim;
321
322 if state.fitness.is_empty() {
323 state.fitness.clone_from(&fitness_host);
324 let best_idx = argmin(&fitness_host);
325 state.best_fitness = fitness_host[best_idx];
326 #[allow(clippy::cast_possible_wrap)]
327 let idx = Tensor::<B, 1, Int>::from_data(
328 TensorData::new(vec![best_idx as i64], [1]),
329 &device,
330 );
331 state.best_genome = Some(candidates.clone().select(0, idx));
332 state.positions = candidates;
333 state.generation += 1;
334 let m = StrategyMetrics::from_host_fitness(
335 state.generation,
336 &fitness_host,
337 state.best_fitness,
338 );
339 state.best_fitness = m.best_fitness_ever;
340 return (state, m);
341 }
342
343 #[allow(clippy::cast_possible_wrap)]
346 let mut rs: Vec<i64> = (0..pop).map(|i| i as i64).collect();
347 let mut new_fitness = state.fitness.clone();
348 #[allow(clippy::cast_precision_loss)]
349 let t = state.generation as f32;
350 for i in 0..pop {
351 let accept_gate = state.pending_accept.get(i).copied().unwrap_or(false);
352 let improves = fitness_host[i] <= state.fitness[i];
353 if accept_gate && improves {
354 #[allow(clippy::cast_possible_wrap)]
355 {
356 rs[i] = (pop + i) as i64;
357 }
358 new_fitness[i] = fitness_host[i];
359 state.loudness[i] *= params.alpha;
360 state.pulse_rate[i] = params.r0 * (1.0 - (-params.gamma * t).exp());
361 }
362 }
363 let stacked = Tensor::cat(vec![state.positions.clone(), candidates], 0);
364 let idx = Tensor::<B, 1, Int>::from_data(TensorData::new(rs, [pop]), &device);
365 state.positions = stacked.select(0, idx);
366 state.fitness = new_fitness;
367
368 let best_idx = argmin(&state.fitness);
370 if state.fitness[best_idx] < state.best_fitness {
371 state.best_fitness = state.fitness[best_idx];
372 #[allow(clippy::cast_possible_wrap)]
373 let idx = Tensor::<B, 1, Int>::from_data(
374 TensorData::new(vec![best_idx as i64], [1]),
375 &device,
376 );
377 state.best_genome = Some(state.positions.clone().select(0, idx));
378 }
379
380 state.generation += 1;
381 let m =
382 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
383 state.best_fitness = m.best_fitness_ever;
384 let _ = genome_dim;
385 (state, m)
386 }
387
388 fn best(&self, state: &BatState<B>) -> Option<(Tensor<B, 2>, f32)> {
391 state
392 .best_genome
393 .as_ref()
394 .map(|g| (g.clone(), state.best_fitness))
395 }
396}
397
398fn argmin(xs: &[f32]) -> usize {
399 let mut best_idx = 0usize;
400 let mut best = f32::INFINITY;
401 for (i, &v) in xs.iter().enumerate() {
402 if v < best {
403 best = v;
404 best_idx = i;
405 }
406 }
407 best_idx
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::fitness::FromFitnessEvaluable;
414 use crate::strategy::EvolutionaryHarness;
415 use burn::backend::Flex;
416 use rlevo_core::fitness::FitnessEvaluable;
417
418 type TestBackend = Flex;
419
420 struct Sphere;
421 struct SphereFit;
422 impl FitnessEvaluable for SphereFit {
423 type Individual = Vec<f64>;
424 type Landscape = Sphere;
425 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
426 x.iter().map(|v| v * v).sum()
427 }
428 }
429
430 #[test]
431 fn bat_converges_on_sphere_d10() {
432 let device = Default::default();
439 let strategy = BatAlgorithm::<TestBackend>::new();
440 let params = BatConfig::default_for(40, 10);
441 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
442 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
443 strategy, params, fitness_fn, 23, device, 800,
444 );
445 harness.reset();
446 while !harness.step(()).done {}
447 let best = harness.latest_metrics().unwrap().best_fitness_ever;
448 assert!(best < 0.1, "Bat D10 best={best}");
449 }
450}