rlevo_evolution/algorithms/metaheuristic/
bat.rs1use std::marker::PhantomData;
29
30use burn::tensor::{Distribution, 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::Device) -> BatState<B> {
139 let (lo, hi) = params.bounds;
140 B::seed(device, rng.next_u64());
141 let positions = Tensor::<B, 2>::random(
142 [params.pop_size, params.genome_dim],
143 Distribution::Uniform(f64::from(lo), f64::from(hi)),
144 device,
145 );
146 let velocities = Tensor::<B, 2>::zeros([params.pop_size, params.genome_dim], device);
147 BatState {
148 positions,
149 velocities,
150 loudness: vec![params.a0; params.pop_size],
151 pulse_rate: vec![params.r0; params.pop_size],
152 fitness: Vec::new(),
153 best_genome: None,
154 best_fitness: f32::INFINITY,
155 generation: 0,
156 pending_accept: Vec::new(),
157 }
158 }
159
160 fn ask(
161 &self,
162 params: &BatConfig,
163 state: &BatState<B>,
164 rng: &mut dyn Rng,
165 device: &B::Device,
166 ) -> (Tensor<B, 2>, BatState<B>) {
167 if state.fitness.is_empty() {
168 return (state.positions.clone(), state.clone());
171 }
172
173 let pop = params.pop_size;
174 let genome_dim = params.genome_dim;
175 let (lo, hi) = params.bounds;
176
177 let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
182
183 let mut betas = Vec::with_capacity(pop);
184 let mut use_local = Vec::with_capacity(pop);
185 let mut accept_draw = Vec::with_capacity(pop);
186 let mut epsilon_rows = Vec::with_capacity(pop * genome_dim);
187 for i in 0..pop {
188 betas.push(stream.random::<f32>());
189 use_local.push(stream.random::<f32>() > state.pulse_rate[i]);
190 accept_draw.push(stream.random::<f32>());
191 for _ in 0..genome_dim {
192 epsilon_rows.push(2.0 * stream.random::<f32>() - 1.0);
193 }
194 }
195
196 let mean_loudness: f32 = {
199 let s: f32 = state.loudness.iter().sum();
200 #[allow(clippy::cast_precision_loss)]
201 {
202 s / pop as f32
203 }
204 };
205
206 let best = state
207 .best_genome
208 .as_ref()
209 .expect("best populated after first tell")
210 .clone()
211 .expand([pop, genome_dim]);
212
213 let f_vec: Vec<f32> = betas
215 .iter()
216 .map(|b| params.f_min + (params.f_max - params.f_min) * b)
217 .collect();
218 let f_mat = Tensor::<B, 1>::from_data(TensorData::new(f_vec, [pop]), device)
219 .unsqueeze_dim::<2>(1)
220 .expand([pop, genome_dim]);
221
222 let new_velocities =
223 state.velocities.clone() + (state.positions.clone() - best.clone()).mul(f_mat);
224 let global_move = state.positions.clone() + new_velocities.clone();
225 let eps =
227 Tensor::<B, 2>::from_data(TensorData::new(epsilon_rows, [pop, genome_dim]), device);
228 let local_move = best + eps.mul_scalar(mean_loudness);
229
230 #[allow(clippy::cast_possible_wrap)]
231 let mask = Tensor::<B, 1, Int>::from_data(
232 TensorData::new(
233 use_local.iter().map(|&b| i64::from(b)).collect::<Vec<_>>(),
234 [pop],
235 ),
236 device,
237 )
238 .equal_elem(1)
239 .unsqueeze_dim::<2>(1)
240 .expand([pop, genome_dim]);
241 let candidates = global_move.mask_where(mask, local_move).clamp(lo, hi);
242
243 let mut next = state.clone();
245 next.velocities = new_velocities;
246 next.pending_accept = accept_draw
247 .iter()
248 .zip(state.loudness.iter())
249 .map(|(&draw, &a)| draw < a)
250 .collect();
251 (candidates, next)
252 }
253
254 fn tell(
255 &self,
256 params: &BatConfig,
257 candidates: Tensor<B, 2>,
258 fitness: Tensor<B, 1>,
259 mut state: BatState<B>,
260 _rng: &mut dyn Rng,
261 ) -> (BatState<B>, StrategyMetrics) {
262 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
263 let device = candidates.device();
264 let pop = params.pop_size;
265 let genome_dim = params.genome_dim;
266
267 if state.fitness.is_empty() {
268 state.fitness.clone_from(&fitness_host);
269 let best_idx = argmin(&fitness_host);
270 state.best_fitness = fitness_host[best_idx];
271 #[allow(clippy::cast_possible_wrap)]
272 let idx = Tensor::<B, 1, Int>::from_data(
273 TensorData::new(vec![best_idx as i64], [1]),
274 &device,
275 );
276 state.best_genome = Some(candidates.clone().select(0, idx));
277 state.positions = candidates;
278 state.generation += 1;
279 let m = StrategyMetrics::from_host_fitness(
280 state.generation,
281 &fitness_host,
282 state.best_fitness,
283 );
284 state.best_fitness = m.best_fitness_ever;
285 return (state, m);
286 }
287
288 #[allow(clippy::cast_possible_wrap)]
291 let mut rs: Vec<i64> = (0..pop).map(|i| i as i64).collect();
292 let mut new_fitness = state.fitness.clone();
293 #[allow(clippy::cast_precision_loss)]
294 let t = state.generation as f32;
295 for i in 0..pop {
296 let accept_gate = state.pending_accept.get(i).copied().unwrap_or(false);
297 let improves = fitness_host[i] <= state.fitness[i];
298 if accept_gate && improves {
299 #[allow(clippy::cast_possible_wrap)]
300 {
301 rs[i] = (pop + i) as i64;
302 }
303 new_fitness[i] = fitness_host[i];
304 state.loudness[i] *= params.alpha;
305 state.pulse_rate[i] = params.r0 * (1.0 - (-params.gamma * t).exp());
306 }
307 }
308 let stacked = Tensor::cat(vec![state.positions.clone(), candidates], 0);
309 let idx = Tensor::<B, 1, Int>::from_data(TensorData::new(rs, [pop]), &device);
310 state.positions = stacked.select(0, idx);
311 state.fitness = new_fitness;
312
313 let best_idx = argmin(&state.fitness);
315 if state.fitness[best_idx] < state.best_fitness {
316 state.best_fitness = state.fitness[best_idx];
317 #[allow(clippy::cast_possible_wrap)]
318 let idx = Tensor::<B, 1, Int>::from_data(
319 TensorData::new(vec![best_idx as i64], [1]),
320 &device,
321 );
322 state.best_genome = Some(state.positions.clone().select(0, idx));
323 }
324
325 state.generation += 1;
326 let m =
327 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
328 state.best_fitness = m.best_fitness_ever;
329 let _ = genome_dim;
330 (state, m)
331 }
332
333 fn best(&self, state: &BatState<B>) -> Option<(Tensor<B, 2>, f32)> {
334 state
335 .best_genome
336 .as_ref()
337 .map(|g| (g.clone(), state.best_fitness))
338 }
339}
340
341fn argmin(xs: &[f32]) -> usize {
342 let mut best_idx = 0usize;
343 let mut best = f32::INFINITY;
344 for (i, &v) in xs.iter().enumerate() {
345 if v < best {
346 best = v;
347 best_idx = i;
348 }
349 }
350 best_idx
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::fitness::FromFitnessEvaluable;
357 use crate::strategy::EvolutionaryHarness;
358 use burn::backend::NdArray;
359 use rlevo_core::fitness::FitnessEvaluable;
360
361 type TestBackend = NdArray;
362
363 struct Sphere;
364 struct SphereFit;
365 impl FitnessEvaluable for SphereFit {
366 type Individual = Vec<f64>;
367 type Landscape = Sphere;
368 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
369 x.iter().map(|v| v * v).sum()
370 }
371 }
372
373 #[test]
374 fn bat_converges_on_sphere_d10() {
375 let device = Default::default();
382 let strategy = BatAlgorithm::<TestBackend>::new();
383 let params = BatConfig::default_for(40, 10);
384 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
385 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
386 strategy, params, fitness_fn, 23, device, 800,
387 );
388 harness.reset();
389 while !harness.step(()).done {}
390 let best = harness.latest_metrics().unwrap().best_fitness_ever;
391 assert!(best < 0.1, "Bat D10 best={best}");
392 }
393}