use std::marker::PhantomData;
use burn::tensor::{Distribution, Int, Tensor, TensorData, backend::Backend};
use rand::Rng;
use rand::RngExt;
use crate::rng::{SeedPurpose, seed_stream};
use crate::strategy::{Strategy, StrategyMetrics};
#[derive(Debug, Clone)]
pub struct AbcConfig {
pub pop_size: usize,
pub genome_dim: usize,
pub bounds: (f32, f32),
pub limit: usize,
pub tournament_size: usize,
}
impl AbcConfig {
#[must_use]
pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
Self {
pop_size,
genome_dim,
bounds: (-5.12, 5.12),
limit: (pop_size * genome_dim) / 2,
tournament_size: 3,
}
}
}
#[derive(Debug, Clone)]
pub struct AbcState<B: Backend> {
pub colony: Tensor<B, 2>,
pub fitness: Vec<f32>,
pub trial: Vec<usize>,
pub target_of_candidate: Vec<usize>,
pub best_genome: Option<Tensor<B, 2>>,
pub best_fitness: f32,
pub generation: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ArtificialBeeColony<B: Backend> {
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend> ArtificialBeeColony<B> {
#[must_use]
pub fn new() -> Self {
Self {
_backend: PhantomData,
}
}
#[allow(clippy::too_many_arguments)]
fn build_candidates(
targets: &[usize],
neighbors: &[usize],
dims: &[usize],
phi: &[f32],
colony: &Tensor<B, 2>,
pop_size: usize,
genome_dim: usize,
device: &B::Device,
) -> Tensor<B, 2> {
#[allow(clippy::cast_possible_wrap)]
let target_idx: Vec<i64> = targets.iter().map(|&i| i as i64).collect();
let _ = pop_size; let n_cand = targets.len();
let target_tensor =
Tensor::<B, 1, Int>::from_data(TensorData::new(target_idx, [n_cand]), device);
let base = colony.clone().select(0, target_tensor);
#[allow(clippy::cast_possible_wrap)]
let neighbor_idx: Vec<i64> = neighbors.iter().map(|&i| i as i64).collect();
let neighbor_tensor =
Tensor::<B, 1, Int>::from_data(TensorData::new(neighbor_idx, [n_cand]), device);
let neighbor_rows = colony.clone().select(0, neighbor_tensor);
let mut mask = vec![0i64; n_cand * genome_dim];
for (row, &j) in dims.iter().enumerate() {
mask[row * genome_dim + j] = 1;
}
let mask_bool =
Tensor::<B, 2, Int>::from_data(TensorData::new(mask, [n_cand, genome_dim]), device)
.equal_elem(1);
let phi_row = Tensor::<B, 1>::from_data(TensorData::new(phi.to_vec(), [n_cand]), device)
.unsqueeze_dim::<2>(1)
.expand([n_cand, genome_dim]);
let delta = phi_row.mul(base.clone() - neighbor_rows);
let perturbed = base.clone() + delta;
base.mask_where(mask_bool, perturbed)
}
}
impl<B: Backend> Strategy<B> for ArtificialBeeColony<B>
where
B::Device: Clone,
{
type Params = AbcConfig;
type State = AbcState<B>;
type Genome = Tensor<B, 2>;
fn init(&self, params: &AbcConfig, rng: &mut dyn Rng, device: &B::Device) -> AbcState<B> {
assert!(params.pop_size >= 2, "ABC requires pop_size >= 2");
let (lo, hi) = params.bounds;
B::seed(device, rng.next_u64());
let colony = Tensor::<B, 2>::random(
[params.pop_size, params.genome_dim],
Distribution::Uniform(f64::from(lo), f64::from(hi)),
device,
);
AbcState {
colony,
fitness: Vec::new(),
trial: vec![0; params.pop_size],
target_of_candidate: Vec::new(),
best_genome: None,
best_fitness: f32::INFINITY,
generation: 0,
}
}
fn ask(
&self,
params: &AbcConfig,
state: &AbcState<B>,
rng: &mut dyn Rng,
device: &B::Device,
) -> (Tensor<B, 2>, AbcState<B>) {
if state.fitness.is_empty() {
return (state.colony.clone(), state.clone());
}
let pop = params.pop_size;
let genome_dim = params.genome_dim;
let n_cand = 2 * pop;
let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
let mut targets = Vec::with_capacity(n_cand);
let mut neighbors = Vec::with_capacity(n_cand);
let mut dims = Vec::with_capacity(n_cand);
let mut phis = Vec::with_capacity(n_cand);
for i in 0..pop {
targets.push(i);
}
for _ in 0..pop {
let mut best = stream.random_range(0..pop);
for _ in 1..params.tournament_size {
let c = stream.random_range(0..pop);
if state.fitness[c] < state.fitness[best] {
best = c;
}
}
targets.push(best);
}
for &t in &targets {
let mut k = stream.random_range(0..pop);
if k == t {
k = (k + 1) % pop;
}
neighbors.push(k);
dims.push(stream.random_range(0..genome_dim));
let phi = 2.0 * stream.random::<f32>() - 1.0;
phis.push(phi);
}
let candidates = Self::build_candidates(
&targets,
&neighbors,
&dims,
&phis,
&state.colony,
pop,
genome_dim,
device,
);
let (lo, hi) = params.bounds;
let candidates = candidates.clamp(lo, hi);
let mut next = state.clone();
next.target_of_candidate = targets;
(candidates, next)
}
#[allow(clippy::too_many_lines)]
fn tell(
&self,
params: &AbcConfig,
candidates: Tensor<B, 2>,
fitness: Tensor<B, 1>,
mut state: AbcState<B>,
rng: &mut dyn Rng,
) -> (AbcState<B>, StrategyMetrics) {
let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
let device = candidates.device();
let pop = params.pop_size;
let genome_dim = params.genome_dim;
if state.fitness.is_empty() {
state.fitness.clone_from(&fitness_host);
let best_idx = argmin(&fitness_host);
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(candidates.clone().select(0, idx));
state.colony = candidates;
state.generation += 1;
let m = StrategyMetrics::from_host_fitness(
state.generation,
&fitness_host,
state.best_fitness,
);
state.best_fitness = m.best_fitness_ever;
return (state, m);
}
let mut best_per_target: Vec<Option<(usize, f32)>> = vec![None; pop];
for (cand_idx, &t) in state.target_of_candidate.iter().enumerate() {
let cand_fit = fitness_host[cand_idx];
if cand_fit <= state.fitness[t] {
match best_per_target[t] {
None => best_per_target[t] = Some((cand_idx, cand_fit)),
Some((_, prev)) if cand_fit < prev => {
best_per_target[t] = Some((cand_idx, cand_fit));
}
_ => {}
}
}
}
let stacked = Tensor::cat(vec![state.colony.clone(), candidates.clone()], 0);
#[allow(clippy::cast_possible_wrap)]
let mut rs: Vec<i64> = (0..pop).map(|i| i as i64).collect();
let mut new_fitness = state.fitness.clone();
for t in 0..pop {
match best_per_target[t] {
Some((cand_idx, cand_fit)) => {
#[allow(clippy::cast_possible_wrap)]
{
rs[t] = (pop + cand_idx) as i64;
}
new_fitness[t] = cand_fit;
state.trial[t] = 0;
}
None => {
state.trial[t] += 1;
}
}
}
let idx = Tensor::<B, 1, Int>::from_data(TensorData::new(rs, [pop]), &device);
state.colony = stacked.select(0, idx);
state.fitness = new_fitness;
let mut scouts: Vec<usize> = Vec::new();
for (i, trial) in state.trial.iter_mut().enumerate() {
if *trial > params.limit {
scouts.push(i);
*trial = 0;
}
}
if !scouts.is_empty() {
let (lo, hi) = params.bounds;
B::seed(&device, rng.next_u64());
let fresh = Tensor::<B, 2>::random(
[scouts.len(), genome_dim],
Distribution::Uniform(f64::from(lo), f64::from(hi)),
&device,
);
#[allow(clippy::cast_possible_wrap)]
let mut rs2: Vec<i64> = (0..pop).map(|i| i as i64).collect();
for (k, &scout) in scouts.iter().enumerate() {
#[allow(clippy::cast_possible_wrap)]
{
rs2[scout] = (pop + k) as i64;
}
state.fitness[scout] = f32::INFINITY;
}
let stacked2 = Tensor::cat(vec![state.colony.clone(), fresh], 0);
let idx2 = Tensor::<B, 1, Int>::from_data(TensorData::new(rs2, [pop]), &device);
state.colony = stacked2.select(0, idx2);
}
let best_idx = argmin(&state.fitness);
if state.fitness[best_idx].is_finite() && state.fitness[best_idx] < state.best_fitness {
state.best_fitness = state.fitness[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(state.colony.clone().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: &AbcState<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 abc_converges_on_sphere_d10() {
let device = Default::default();
let strategy = ArtificialBeeColony::<TestBackend>::new();
let params = AbcConfig::default_for(30, 10);
let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
strategy, params, fitness_fn, 13, device, 400,
);
harness.reset();
while !harness.step(()).done {}
let best = harness.latest_metrics().unwrap().best_fitness_ever;
assert!(best < 1e-4, "ABC D10 best={best}");
}
}