rlevo_evolution/algorithms/metaheuristic/
woa.rs1use std::f32::consts::PI;
33use std::marker::PhantomData;
34
35use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
36use rand::Rng;
37use rand::RngExt;
38
39use crate::rng::{SeedPurpose, seed_stream};
40use crate::strategy::{Strategy, StrategyMetrics};
41
42#[derive(Debug, Clone)]
44pub struct WoaConfig {
45 pub pop_size: usize,
47 pub genome_dim: usize,
49 pub bounds: (f32, f32),
51 pub max_generations: usize,
53 pub b: f32,
55}
56
57impl WoaConfig {
58 #[must_use]
60 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
61 Self {
62 pop_size,
63 genome_dim,
64 bounds: (-5.12, 5.12),
65 max_generations: 500,
66 b: 1.0,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct WoaState<B: Backend> {
74 pub positions: Tensor<B, 2>,
76 pub fitness: Vec<f32>,
78 pub best_genome: Option<Tensor<B, 2>>,
80 pub best_fitness: f32,
82 pub generation: usize,
84}
85
86#[derive(Debug, Clone, Copy, Default)]
109pub struct WhaleOptimization<B: Backend> {
110 _backend: PhantomData<fn() -> B>,
111}
112
113impl<B: Backend> WhaleOptimization<B> {
114 #[must_use]
116 pub fn new() -> Self {
117 Self {
118 _backend: PhantomData,
119 }
120 }
121}
122
123impl<B: Backend> Strategy<B> for WhaleOptimization<B>
124where
125 B::Device: Clone,
126{
127 type Params = WoaConfig;
128 type State = WoaState<B>;
129 type Genome = Tensor<B, 2>;
130
131 fn init(&self, params: &WoaConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> WoaState<B> {
135 let (lo, hi) = params.bounds;
136 let pop = params.pop_size;
141 let genome_dim = params.genome_dim;
142 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
143 let mut position_rows = Vec::with_capacity(pop * genome_dim);
144 for _ in 0..pop * genome_dim {
145 position_rows.push(lo + (hi - lo) * stream.random::<f32>());
146 }
147 let positions =
148 Tensor::<B, 2>::from_data(TensorData::new(position_rows, [pop, genome_dim]), device);
149 WoaState {
150 positions,
151 fitness: Vec::new(),
152 best_genome: None,
153 best_fitness: f32::INFINITY,
154 generation: 0,
155 }
156 }
157
158 #[allow(clippy::many_single_char_names)]
172 fn ask(
173 &self,
174 params: &WoaConfig,
175 state: &WoaState<B>,
176 rng: &mut dyn Rng,
177 device: &<B as burn::tensor::backend::BackendTypes>::Device,
178 ) -> (Tensor<B, 2>, WoaState<B>) {
179 if state.fitness.is_empty() {
181 return (state.positions.clone(), state.clone());
182 }
183
184 let pop_size = params.pop_size;
185 let genome_dim = params.genome_dim;
186
187 #[allow(clippy::cast_precision_loss)]
189 let t = state.generation as f32;
190 #[allow(clippy::cast_precision_loss)]
191 let max_t = params.max_generations.max(1) as f32;
192 let a = 2.0 * (1.0 - (t / max_t).min(1.0));
193
194 let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
197 let mut rand_idx: Vec<i64> = Vec::with_capacity(pop_size);
198 let mut a_scalar: Vec<f32> = Vec::with_capacity(pop_size);
199 let mut c_scalar: Vec<f32> = Vec::with_capacity(pop_size);
200 let mut p_scalar: Vec<f32> = Vec::with_capacity(pop_size);
201 let mut l_scalar: Vec<f32> = Vec::with_capacity(pop_size);
202 let mut abs_a_lt_one: Vec<i64> = Vec::with_capacity(pop_size);
203 let mut p_lt_half: Vec<i64> = Vec::with_capacity(pop_size);
204 for i in 0..pop_size {
205 let r_a: f32 = stream.random::<f32>();
206 let r_c: f32 = stream.random::<f32>();
207 let p: f32 = stream.random::<f32>();
208 let l: f32 = 2.0 * stream.random::<f32>() - 1.0;
209 let a_val = 2.0 * a * r_a - a;
210 let c_val = 2.0 * r_c;
211 a_scalar.push(a_val);
212 c_scalar.push(c_val);
213 p_scalar.push(p);
214 l_scalar.push(l);
215 abs_a_lt_one.push(i64::from(a_val.abs() < 1.0));
216 p_lt_half.push(i64::from(p < 0.5));
217 let mut r = stream.random_range(0..pop_size);
219 if r == i {
220 r = (r + 1) % pop_size;
221 }
222 #[allow(clippy::cast_possible_wrap)]
223 rand_idx.push(r as i64);
224 }
225
226 let a_row = Tensor::<B, 1>::from_data(TensorData::new(a_scalar, [pop_size]), device)
227 .unsqueeze_dim::<2>(1)
228 .expand([pop_size, genome_dim]);
229 let c_row = Tensor::<B, 1>::from_data(TensorData::new(c_scalar, [pop_size]), device)
230 .unsqueeze_dim::<2>(1)
231 .expand([pop_size, genome_dim]);
232 let l_vec = Tensor::<B, 1>::from_data(TensorData::new(l_scalar, [pop_size]), device);
233 let rand_idx_t =
234 Tensor::<B, 1, Int>::from_data(TensorData::new(rand_idx, [pop_size]), device);
235 let x_rand = state.positions.clone().select(0, rand_idx_t);
236
237 let x_best = state
238 .best_genome
239 .as_ref()
240 .expect("best_genome populated after the first tell")
241 .clone()
242 .expand([pop_size, genome_dim]);
243
244 let enc_best = x_best.clone()
246 - a_row
247 .clone()
248 .mul((c_row.clone().mul(x_best.clone()) - state.positions.clone()).abs());
249 let enc_rand =
251 x_rand.clone() - a_row.mul((c_row.mul(x_rand) - state.positions.clone()).abs());
252 let dist = (x_best.clone() - state.positions.clone()).abs();
254 let factor = l_vec
255 .clone()
256 .mul_scalar(params.b)
257 .exp()
258 .mul(l_vec.mul_scalar(2.0 * PI).cos());
259 let factor_mat = factor.unsqueeze_dim::<2>(1).expand([pop_size, genome_dim]);
260 let spiral = dist.mul(factor_mat) + x_best;
261
262 let m_abs_a_lt_one =
264 Tensor::<B, 1, Int>::from_data(TensorData::new(abs_a_lt_one, [pop_size]), device)
265 .equal_elem(1)
266 .unsqueeze_dim::<2>(1)
267 .expand([pop_size, genome_dim]);
268 let m_p_lt_half =
269 Tensor::<B, 1, Int>::from_data(TensorData::new(p_lt_half, [pop_size]), device)
270 .equal_elem(1)
271 .unsqueeze_dim::<2>(1)
272 .expand([pop_size, genome_dim]);
273
274 let encircle = enc_rand.mask_where(m_abs_a_lt_one, enc_best);
275 let new_positions = spiral.mask_where(m_p_lt_half, encircle);
276
277 let (lo, hi) = params.bounds;
278 let new_positions = new_positions.clamp(lo, hi);
279
280 let mut next = state.clone();
281 next.positions.clone_from(&new_positions);
282 (new_positions, next)
283 }
284
285 fn tell(
291 &self,
292 _params: &WoaConfig,
293 population: Tensor<B, 2>,
294 fitness: Tensor<B, 1>,
295 mut state: WoaState<B>,
296 _rng: &mut dyn Rng,
297 ) -> (WoaState<B>, StrategyMetrics) {
298 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
299 state.fitness.clone_from(&fitness_host);
300 state.positions.clone_from(&population);
301 let best_idx = argmin(&fitness_host);
302 if fitness_host[best_idx] < state.best_fitness {
303 state.best_fitness = fitness_host[best_idx];
304 let device = population.device();
305 #[allow(clippy::cast_possible_wrap)]
306 let idx = Tensor::<B, 1, Int>::from_data(
307 TensorData::new(vec![best_idx as i64], [1]),
308 &device,
309 );
310 state.best_genome = Some(population.select(0, idx));
311 }
312 state.generation += 1;
313 let m =
314 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
315 state.best_fitness = m.best_fitness_ever;
316 (state, m)
317 }
318
319 fn best(&self, state: &WoaState<B>) -> Option<(Tensor<B, 2>, f32)> {
322 state
323 .best_genome
324 .as_ref()
325 .map(|g| (g.clone(), state.best_fitness))
326 }
327}
328
329fn argmin(xs: &[f32]) -> usize {
330 let mut best_idx = 0usize;
331 let mut best = f32::INFINITY;
332 for (i, &v) in xs.iter().enumerate() {
333 if v < best {
334 best = v;
335 best_idx = i;
336 }
337 }
338 best_idx
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::fitness::FromFitnessEvaluable;
345 use crate::strategy::EvolutionaryHarness;
346 use burn::backend::Flex;
347 use rlevo_core::fitness::FitnessEvaluable;
348
349 type TestBackend = Flex;
350
351 struct Sphere;
352 struct SphereFit;
353 impl FitnessEvaluable for SphereFit {
354 type Individual = Vec<f64>;
355 type Landscape = Sphere;
356 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
357 x.iter().map(|v| v * v).sum()
358 }
359 }
360
361 #[test]
362 fn woa_converges_on_sphere_d10() {
363 let device = Default::default();
368 let strategy = WhaleOptimization::<TestBackend>::new();
369 let params = WoaConfig::default_for(32, 10);
370 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
371 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
372 strategy, params, fitness_fn, 5, device, 600,
373 );
374 harness.reset();
375 while !harness.step(()).done {}
376 let best = harness.latest_metrics().unwrap().best_fitness_ever;
377 assert!(best < 1e-4, "WOA D10 best={best}");
378 }
379}