rlevo_evolution/algorithms/metaheuristic/
firefly.rs1use std::marker::PhantomData;
25
26use burn::tensor::{Distribution, Int, Tensor, TensorData, backend::Backend};
27use rand::Rng;
28
29use crate::rng::{SeedPurpose, seed_stream};
30use crate::strategy::{Strategy, StrategyMetrics};
31
32pub const FIREFLY_PURE_TENSOR_CAP: usize = 128;
36
37#[derive(Debug, Clone)]
39pub struct FireflyConfig {
40 pub pop_size: usize,
42 pub genome_dim: usize,
44 pub bounds: (f32, f32),
46 pub beta0: f32,
48 pub gamma: f32,
51 pub alpha: f32,
53}
54
55impl FireflyConfig {
56 #[must_use]
62 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
63 Self {
64 pop_size,
65 genome_dim,
66 bounds: (-5.12, 5.12),
67 beta0: 1.0,
68 gamma: 0.01,
69 alpha: 0.2,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct FireflyState<B: Backend> {
77 pub positions: Tensor<B, 2>,
79 pub fitness: Vec<f32>,
81 pub best_genome: Option<Tensor<B, 2>>,
83 pub best_fitness: f32,
85 pub generation: usize,
87}
88
89#[derive(Debug, Clone, Copy, Default)]
112pub struct FireflyAlgorithm<B: Backend> {
113 _backend: PhantomData<fn() -> B>,
114}
115
116impl<B: Backend> FireflyAlgorithm<B> {
117 #[must_use]
119 pub fn new() -> Self {
120 Self {
121 _backend: PhantomData,
122 }
123 }
124
125 fn pure_tensor_attract(
130 positions: &Tensor<B, 2>,
131 fitness: &[f32],
132 beta0: f32,
133 gamma: f32,
134 alpha: f32,
135 device: &B::Device,
136 noise_seed: u64,
137 ) -> Tensor<B, 2> {
138 let pop = fitness.len();
139 let shape = positions.shape().dims;
140 let d = shape[1];
141
142 let xi = positions.clone().unsqueeze_dim::<3>(1); let xj = positions.clone().unsqueeze_dim::<3>(0); let diff = xj.expand([pop, pop, d]) - xi.expand([pop, pop, d]); let r2 = diff.clone().powi_scalar(2).sum_dim(2).squeeze::<2>(); let beta = r2.mul_scalar(-gamma).exp().mul_scalar(beta0); let mut bright = vec![0i64; pop * pop];
154 for i in 0..pop {
155 for j in 0..pop {
156 if fitness[j] < fitness[i] {
157 bright[i * pop + j] = 1;
158 }
159 }
160 }
161 let bright_mask =
162 Tensor::<B, 2, Int>::from_data(TensorData::new(bright, [pop, pop]), device)
163 .equal_elem(1);
164 let zero = Tensor::<B, 2>::zeros([pop, pop], device);
166 let beta_m = beta.mask_where(bright_mask.bool_not(), zero);
167 let weight = beta_m.unsqueeze_dim::<3>(2).expand([pop, pop, d]); let weighted = diff.mul(weight); let attr_sum = weighted.sum_dim(1).squeeze::<2>(); B::seed(device, noise_seed);
173 let noise = Tensor::<B, 2>::random([pop, d], Distribution::Uniform(-0.5, 0.5), device);
174 attr_sum + noise.mul_scalar(alpha)
175 }
176}
177
178impl<B: Backend> Strategy<B> for FireflyAlgorithm<B>
179where
180 B::Device: Clone,
181{
182 type Params = FireflyConfig;
183 type State = FireflyState<B>;
184 type Genome = Tensor<B, 2>;
185
186 fn init(
187 &self,
188 params: &FireflyConfig,
189 rng: &mut dyn Rng,
190 device: &B::Device,
191 ) -> FireflyState<B> {
192 #[cfg(not(feature = "custom-kernels"))]
193 assert!(
194 params.pop_size <= FIREFLY_PURE_TENSOR_CAP,
195 "Firefly without `custom-kernels` feature caps pop_size at {} to keep the O(N²D) \
196 pairwise tensor bounded; enable `custom-kernels` for larger swarms",
197 FIREFLY_PURE_TENSOR_CAP
198 );
199 #[cfg(feature = "custom-kernels")]
205 debug_assert!(
206 params.pop_size <= FIREFLY_PURE_TENSOR_CAP,
207 "Firefly pop_size > {FIREFLY_PURE_TENSOR_CAP} requires the fused pairwise-attract kernel; \
208 the placeholder kernel module still runs the pure-tensor path"
209 );
210 let (lo, hi) = params.bounds;
211 B::seed(device, rng.next_u64());
212 let positions = Tensor::<B, 2>::random(
213 [params.pop_size, params.genome_dim],
214 Distribution::Uniform(f64::from(lo), f64::from(hi)),
215 device,
216 );
217 FireflyState {
218 positions,
219 fitness: Vec::new(),
220 best_genome: None,
221 best_fitness: f32::INFINITY,
222 generation: 0,
223 }
224 }
225
226 fn ask(
227 &self,
228 params: &FireflyConfig,
229 state: &FireflyState<B>,
230 rng: &mut dyn Rng,
231 device: &B::Device,
232 ) -> (Tensor<B, 2>, FireflyState<B>) {
233 if state.fitness.is_empty() {
234 return (state.positions.clone(), state.clone());
235 }
236
237 let seed = seed_stream(
238 rng.next_u64(),
239 state.generation as u64,
240 SeedPurpose::Mutation,
241 )
242 .next_u64();
243 let delta = Self::pure_tensor_attract(
244 &state.positions,
245 &state.fitness,
246 params.beta0,
247 params.gamma,
248 params.alpha,
249 device,
250 seed,
251 );
252 let (lo, hi) = params.bounds;
253 let new_positions = (state.positions.clone() + delta).clamp(lo, hi);
254
255 let mut next = state.clone();
256 next.positions.clone_from(&new_positions);
257 (new_positions, next)
258 }
259
260 fn tell(
261 &self,
262 _params: &FireflyConfig,
263 population: Tensor<B, 2>,
264 fitness: Tensor<B, 1>,
265 mut state: FireflyState<B>,
266 _rng: &mut dyn Rng,
267 ) -> (FireflyState<B>, StrategyMetrics) {
268 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
269 let device = population.device();
270 state.fitness.clone_from(&fitness_host);
271 state.positions.clone_from(&population);
272
273 let best_idx = argmin(&fitness_host);
274 if fitness_host[best_idx] < state.best_fitness {
275 state.best_fitness = fitness_host[best_idx];
276 #[allow(clippy::cast_possible_wrap)]
277 let idx = Tensor::<B, 1, Int>::from_data(
278 TensorData::new(vec![best_idx as i64], [1]),
279 &device,
280 );
281 state.best_genome = Some(population.select(0, idx));
282 }
283 state.generation += 1;
284 let m =
285 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
286 state.best_fitness = m.best_fitness_ever;
287 (state, m)
288 }
289
290 fn best(&self, state: &FireflyState<B>) -> Option<(Tensor<B, 2>, f32)> {
291 state
292 .best_genome
293 .as_ref()
294 .map(|g| (g.clone(), state.best_fitness))
295 }
296}
297
298fn argmin(xs: &[f32]) -> usize {
299 let mut best_idx = 0usize;
300 let mut best = f32::INFINITY;
301 for (i, &v) in xs.iter().enumerate() {
302 if v < best {
303 best = v;
304 best_idx = i;
305 }
306 }
307 best_idx
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::fitness::FromFitnessEvaluable;
314 use crate::strategy::EvolutionaryHarness;
315 use burn::backend::NdArray;
316 use rlevo_core::fitness::FitnessEvaluable;
317
318 type TestBackend = NdArray;
319
320 struct Sphere;
321 struct SphereFit;
322 impl FitnessEvaluable for SphereFit {
323 type Individual = Vec<f64>;
324 type Landscape = Sphere;
325 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
326 x.iter().map(|v| v * v).sum()
327 }
328 }
329
330 #[test]
331 fn firefly_converges_on_sphere_d10() {
332 let device = Default::default();
336 let strategy = FireflyAlgorithm::<TestBackend>::new();
337 let params = FireflyConfig::default_for(24, 10);
338 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
339 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
340 strategy, params, fitness_fn, 29, device, 500,
341 );
342 harness.reset();
343 while !harness.step(()).done {}
344 let best = harness.latest_metrics().unwrap().best_fitness_ever;
345 assert!(best < 1.0, "Firefly D10 best={best}");
346 }
347}