use std::marker::PhantomData;
use burn::tensor::{Distribution, Int, Tensor, TensorData, backend::Backend};
use rand::Rng;
use crate::rng::{SeedPurpose, seed_stream};
use crate::strategy::{Strategy, StrategyMetrics};
pub const FIREFLY_PURE_TENSOR_CAP: usize = 128;
#[derive(Debug, Clone)]
pub struct FireflyConfig {
pub pop_size: usize,
pub genome_dim: usize,
pub bounds: (f32, f32),
pub beta0: f32,
pub gamma: f32,
pub alpha: f32,
}
impl FireflyConfig {
#[must_use]
pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
Self {
pop_size,
genome_dim,
bounds: (-5.12, 5.12),
beta0: 1.0,
gamma: 0.01,
alpha: 0.2,
}
}
}
#[derive(Debug, Clone)]
pub struct FireflyState<B: Backend> {
pub positions: Tensor<B, 2>,
pub fitness: Vec<f32>,
pub best_genome: Option<Tensor<B, 2>>,
pub best_fitness: f32,
pub generation: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FireflyAlgorithm<B: Backend> {
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend> FireflyAlgorithm<B> {
#[must_use]
pub fn new() -> Self {
Self {
_backend: PhantomData,
}
}
fn pure_tensor_attract(
positions: &Tensor<B, 2>,
fitness: &[f32],
beta0: f32,
gamma: f32,
alpha: f32,
device: &B::Device,
noise_seed: u64,
) -> Tensor<B, 2> {
let pop = fitness.len();
let shape = positions.shape().dims;
let d = shape[1];
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];
for i in 0..pop {
for j in 0..pop {
if fitness[j] < fitness[i] {
bright[i * pop + j] = 1;
}
}
}
let bright_mask =
Tensor::<B, 2, Int>::from_data(TensorData::new(bright, [pop, pop]), device)
.equal_elem(1);
let zero = Tensor::<B, 2>::zeros([pop, pop], device);
let beta_m = beta.mask_where(bright_mask.bool_not(), zero);
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);
let noise = Tensor::<B, 2>::random([pop, d], Distribution::Uniform(-0.5, 0.5), device);
attr_sum + noise.mul_scalar(alpha)
}
}
impl<B: Backend> Strategy<B> for FireflyAlgorithm<B>
where
B::Device: Clone,
{
type Params = FireflyConfig;
type State = FireflyState<B>;
type Genome = Tensor<B, 2>;
fn init(
&self,
params: &FireflyConfig,
rng: &mut dyn Rng,
device: &B::Device,
) -> FireflyState<B> {
#[cfg(not(feature = "custom-kernels"))]
assert!(
params.pop_size <= FIREFLY_PURE_TENSOR_CAP,
"Firefly without `custom-kernels` feature caps pop_size at {} to keep the O(N²D) \
pairwise tensor bounded; enable `custom-kernels` for larger swarms",
FIREFLY_PURE_TENSOR_CAP
);
#[cfg(feature = "custom-kernels")]
debug_assert!(
params.pop_size <= FIREFLY_PURE_TENSOR_CAP,
"Firefly pop_size > {FIREFLY_PURE_TENSOR_CAP} requires the fused pairwise-attract kernel; \
the placeholder kernel module still runs the pure-tensor path"
);
let (lo, hi) = params.bounds;
B::seed(device, rng.next_u64());
let positions = Tensor::<B, 2>::random(
[params.pop_size, params.genome_dim],
Distribution::Uniform(f64::from(lo), f64::from(hi)),
device,
);
FireflyState {
positions,
fitness: Vec::new(),
best_genome: None,
best_fitness: f32::INFINITY,
generation: 0,
}
}
fn ask(
&self,
params: &FireflyConfig,
state: &FireflyState<B>,
rng: &mut dyn Rng,
device: &B::Device,
) -> (Tensor<B, 2>, FireflyState<B>) {
if state.fitness.is_empty() {
return (state.positions.clone(), state.clone());
}
let seed = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::Mutation,
)
.next_u64();
let delta = Self::pure_tensor_attract(
&state.positions,
&state.fitness,
params.beta0,
params.gamma,
params.alpha,
device,
seed,
);
let (lo, hi) = params.bounds;
let new_positions = (state.positions.clone() + delta).clamp(lo, hi);
let mut next = state.clone();
next.positions.clone_from(&new_positions);
(new_positions, next)
}
fn tell(
&self,
_params: &FireflyConfig,
population: Tensor<B, 2>,
fitness: Tensor<B, 1>,
mut state: FireflyState<B>,
_rng: &mut dyn Rng,
) -> (FireflyState<B>, StrategyMetrics) {
let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
let device = population.device();
state.fitness.clone_from(&fitness_host);
state.positions.clone_from(&population);
let best_idx = argmin(&fitness_host);
if fitness_host[best_idx] < state.best_fitness {
state.best_fitness = fitness_host[best_idx];
#[allow(clippy::cast_possible_wrap)]
let idx = Tensor::<B, 1, Int>::from_data(
TensorData::new(vec![best_idx as i64], [1]),
&device,
);
state.best_genome = Some(population.select(0, idx));
}
state.generation += 1;
let m =
StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
state.best_fitness = m.best_fitness_ever;
(state, m)
}
fn best(&self, state: &FireflyState<B>) -> Option<(Tensor<B, 2>, f32)> {
state
.best_genome
.as_ref()
.map(|g| (g.clone(), state.best_fitness))
}
}
fn argmin(xs: &[f32]) -> usize {
let mut best_idx = 0usize;
let mut best = f32::INFINITY;
for (i, &v) in xs.iter().enumerate() {
if v < best {
best = v;
best_idx = i;
}
}
best_idx
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fitness::FromFitnessEvaluable;
use crate::strategy::EvolutionaryHarness;
use burn::backend::NdArray;
use rlevo_core::fitness::FitnessEvaluable;
type TestBackend = NdArray;
struct Sphere;
struct SphereFit;
impl FitnessEvaluable for SphereFit {
type Individual = Vec<f64>;
type Landscape = Sphere;
fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
x.iter().map(|v| v * v).sum()
}
}
#[test]
fn firefly_converges_on_sphere_d10() {
let device = Default::default();
let strategy = FireflyAlgorithm::<TestBackend>::new();
let params = FireflyConfig::default_for(24, 10);
let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
strategy, params, fitness_fn, 29, device, 500,
);
harness.reset();
while !harness.step(()).done {}
let best = harness.latest_metrics().unwrap().best_fitness_ever;
assert!(best < 1.0, "Firefly D10 best={best}");
}
}