1use std::marker::PhantomData;
25
26use burn::tensor::{Distribution, Int, Tensor, TensorData, backend::Backend};
27use rand::Rng;
28use rand::RngExt;
29
30use crate::rng::{SeedPurpose, seed_stream};
31use crate::strategy::{Strategy, StrategyMetrics};
32
33#[derive(Debug, Clone)]
35pub struct AbcConfig {
36 pub pop_size: usize,
39 pub genome_dim: usize,
41 pub bounds: (f32, f32),
43 pub limit: usize,
46 pub tournament_size: usize,
50}
51
52impl AbcConfig {
53 #[must_use]
55 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
56 Self {
57 pop_size,
58 genome_dim,
59 bounds: (-5.12, 5.12),
60 limit: (pop_size * genome_dim) / 2,
61 tournament_size: 3,
62 }
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct AbcState<B: Backend> {
69 pub colony: Tensor<B, 2>,
71 pub fitness: Vec<f32>,
73 pub trial: Vec<usize>,
75 pub target_of_candidate: Vec<usize>,
79 pub best_genome: Option<Tensor<B, 2>>,
81 pub best_fitness: f32,
83 pub generation: usize,
85}
86
87#[derive(Debug, Clone, Copy, Default)]
106pub struct ArtificialBeeColony<B: Backend> {
107 _backend: PhantomData<fn() -> B>,
108}
109
110impl<B: Backend> ArtificialBeeColony<B> {
111 #[must_use]
113 pub fn new() -> Self {
114 Self {
115 _backend: PhantomData,
116 }
117 }
118
119 #[allow(clippy::too_many_arguments)]
120 fn build_candidates(
121 targets: &[usize],
122 neighbors: &[usize],
123 dims: &[usize],
124 phi: &[f32],
125 colony: &Tensor<B, 2>,
126 pop_size: usize,
127 genome_dim: usize,
128 device: &B::Device,
129 ) -> Tensor<B, 2> {
130 #[allow(clippy::cast_possible_wrap)]
132 let target_idx: Vec<i64> = targets.iter().map(|&i| i as i64).collect();
133 let _ = pop_size; let n_cand = targets.len();
135 let target_tensor =
136 Tensor::<B, 1, Int>::from_data(TensorData::new(target_idx, [n_cand]), device);
137 let base = colony.clone().select(0, target_tensor);
138
139 #[allow(clippy::cast_possible_wrap)]
141 let neighbor_idx: Vec<i64> = neighbors.iter().map(|&i| i as i64).collect();
142 let neighbor_tensor =
143 Tensor::<B, 1, Int>::from_data(TensorData::new(neighbor_idx, [n_cand]), device);
144 let neighbor_rows = colony.clone().select(0, neighbor_tensor);
145
146 let mut mask = vec![0i64; n_cand * genome_dim];
148 for (row, &j) in dims.iter().enumerate() {
149 mask[row * genome_dim + j] = 1;
150 }
151 let mask_bool =
152 Tensor::<B, 2, Int>::from_data(TensorData::new(mask, [n_cand, genome_dim]), device)
153 .equal_elem(1);
154
155 let phi_row = Tensor::<B, 1>::from_data(TensorData::new(phi.to_vec(), [n_cand]), device)
157 .unsqueeze_dim::<2>(1)
158 .expand([n_cand, genome_dim]);
159 let delta = phi_row.mul(base.clone() - neighbor_rows);
160 let perturbed = base.clone() + delta;
161 base.mask_where(mask_bool, perturbed)
162 }
163}
164
165impl<B: Backend> Strategy<B> for ArtificialBeeColony<B>
166where
167 B::Device: Clone,
168{
169 type Params = AbcConfig;
170 type State = AbcState<B>;
171 type Genome = Tensor<B, 2>;
172
173 fn init(&self, params: &AbcConfig, rng: &mut dyn Rng, device: &B::Device) -> AbcState<B> {
174 assert!(params.pop_size >= 2, "ABC requires pop_size >= 2");
175 let (lo, hi) = params.bounds;
176 B::seed(device, rng.next_u64());
177 let colony = Tensor::<B, 2>::random(
178 [params.pop_size, params.genome_dim],
179 Distribution::Uniform(f64::from(lo), f64::from(hi)),
180 device,
181 );
182 AbcState {
183 colony,
184 fitness: Vec::new(),
185 trial: vec![0; params.pop_size],
186 target_of_candidate: Vec::new(),
187 best_genome: None,
188 best_fitness: f32::INFINITY,
189 generation: 0,
190 }
191 }
192
193 fn ask(
194 &self,
195 params: &AbcConfig,
196 state: &AbcState<B>,
197 rng: &mut dyn Rng,
198 device: &B::Device,
199 ) -> (Tensor<B, 2>, AbcState<B>) {
200 if state.fitness.is_empty() {
201 return (state.colony.clone(), state.clone());
202 }
203
204 let pop = params.pop_size;
205 let genome_dim = params.genome_dim;
206 let n_cand = 2 * pop;
207
208 let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
209
210 let mut targets = Vec::with_capacity(n_cand);
211 let mut neighbors = Vec::with_capacity(n_cand);
212 let mut dims = Vec::with_capacity(n_cand);
213 let mut phis = Vec::with_capacity(n_cand);
214
215 for i in 0..pop {
217 targets.push(i);
218 }
219 for _ in 0..pop {
221 let mut best = stream.random_range(0..pop);
222 for _ in 1..params.tournament_size {
223 let c = stream.random_range(0..pop);
224 if state.fitness[c] < state.fitness[best] {
225 best = c;
226 }
227 }
228 targets.push(best);
229 }
230 for &t in &targets {
232 let mut k = stream.random_range(0..pop);
233 if k == t {
234 k = (k + 1) % pop;
235 }
236 neighbors.push(k);
237 dims.push(stream.random_range(0..genome_dim));
238 let phi = 2.0 * stream.random::<f32>() - 1.0;
239 phis.push(phi);
240 }
241
242 let candidates = Self::build_candidates(
243 &targets,
244 &neighbors,
245 &dims,
246 &phis,
247 &state.colony,
248 pop,
249 genome_dim,
250 device,
251 );
252 let (lo, hi) = params.bounds;
253 let candidates = candidates.clamp(lo, hi);
254
255 let mut next = state.clone();
256 next.target_of_candidate = targets;
257 (candidates, next)
258 }
259
260 #[allow(clippy::too_many_lines)]
261 fn tell(
262 &self,
263 params: &AbcConfig,
264 candidates: Tensor<B, 2>,
265 fitness: Tensor<B, 1>,
266 mut state: AbcState<B>,
267 rng: &mut dyn Rng,
268 ) -> (AbcState<B>, StrategyMetrics) {
269 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
270 let device = candidates.device();
271 let pop = params.pop_size;
272 let genome_dim = params.genome_dim;
273
274 if state.fitness.is_empty() {
276 state.fitness.clone_from(&fitness_host);
277 let best_idx = argmin(&fitness_host);
278 state.best_fitness = fitness_host[best_idx];
279 #[allow(clippy::cast_possible_wrap)]
280 let idx = Tensor::<B, 1, Int>::from_data(
281 TensorData::new(vec![best_idx as i64], [1]),
282 &device,
283 );
284 state.best_genome = Some(candidates.clone().select(0, idx));
285 state.colony = candidates;
286 state.generation += 1;
287 let m = StrategyMetrics::from_host_fitness(
288 state.generation,
289 &fitness_host,
290 state.best_fitness,
291 );
292 state.best_fitness = m.best_fitness_ever;
293 return (state, m);
294 }
295
296 let mut best_per_target: Vec<Option<(usize, f32)>> = vec![None; pop];
299 for (cand_idx, &t) in state.target_of_candidate.iter().enumerate() {
300 let cand_fit = fitness_host[cand_idx];
301 if cand_fit <= state.fitness[t] {
302 match best_per_target[t] {
303 None => best_per_target[t] = Some((cand_idx, cand_fit)),
304 Some((_, prev)) if cand_fit < prev => {
305 best_per_target[t] = Some((cand_idx, cand_fit));
306 }
307 _ => {}
308 }
309 }
310 }
311
312 let stacked = Tensor::cat(vec![state.colony.clone(), candidates.clone()], 0);
317 #[allow(clippy::cast_possible_wrap)]
318 let mut rs: Vec<i64> = (0..pop).map(|i| i as i64).collect();
319 let mut new_fitness = state.fitness.clone();
320 for t in 0..pop {
321 match best_per_target[t] {
322 Some((cand_idx, cand_fit)) => {
323 #[allow(clippy::cast_possible_wrap)]
324 {
325 rs[t] = (pop + cand_idx) as i64;
326 }
327 new_fitness[t] = cand_fit;
328 state.trial[t] = 0;
329 }
330 None => {
331 state.trial[t] += 1;
332 }
333 }
334 }
335 let idx = Tensor::<B, 1, Int>::from_data(TensorData::new(rs, [pop]), &device);
336 state.colony = stacked.select(0, idx);
337 state.fitness = new_fitness;
338
339 let mut scouts: Vec<usize> = Vec::new();
341 for (i, trial) in state.trial.iter_mut().enumerate() {
342 if *trial > params.limit {
343 scouts.push(i);
344 *trial = 0;
345 }
346 }
347 if !scouts.is_empty() {
348 let (lo, hi) = params.bounds;
349 B::seed(&device, rng.next_u64());
350 let fresh = Tensor::<B, 2>::random(
351 [scouts.len(), genome_dim],
352 Distribution::Uniform(f64::from(lo), f64::from(hi)),
353 &device,
354 );
355 #[allow(clippy::cast_possible_wrap)]
357 let mut rs2: Vec<i64> = (0..pop).map(|i| i as i64).collect();
358 for (k, &scout) in scouts.iter().enumerate() {
359 #[allow(clippy::cast_possible_wrap)]
360 {
361 rs2[scout] = (pop + k) as i64;
362 }
363 state.fitness[scout] = f32::INFINITY;
366 }
367 let stacked2 = Tensor::cat(vec![state.colony.clone(), fresh], 0);
368 let idx2 = Tensor::<B, 1, Int>::from_data(TensorData::new(rs2, [pop]), &device);
369 state.colony = stacked2.select(0, idx2);
370 }
371
372 let best_idx = argmin(&state.fitness);
375 if state.fitness[best_idx].is_finite() && state.fitness[best_idx] < state.best_fitness {
376 state.best_fitness = state.fitness[best_idx];
377 #[allow(clippy::cast_possible_wrap)]
378 let idx = Tensor::<B, 1, Int>::from_data(
379 TensorData::new(vec![best_idx as i64], [1]),
380 &device,
381 );
382 state.best_genome = Some(state.colony.clone().select(0, idx));
383 }
384
385 state.generation += 1;
386 let m =
387 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
388 state.best_fitness = m.best_fitness_ever;
389 (state, m)
390 }
391
392 fn best(&self, state: &AbcState<B>) -> Option<(Tensor<B, 2>, f32)> {
393 state
394 .best_genome
395 .as_ref()
396 .map(|g| (g.clone(), state.best_fitness))
397 }
398}
399
400fn argmin(xs: &[f32]) -> usize {
401 let mut best_idx = 0usize;
402 let mut best = f32::INFINITY;
403 for (i, &v) in xs.iter().enumerate() {
404 if v < best {
405 best = v;
406 best_idx = i;
407 }
408 }
409 best_idx
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use crate::fitness::FromFitnessEvaluable;
416 use crate::strategy::EvolutionaryHarness;
417 use burn::backend::NdArray;
418 use rlevo_core::fitness::FitnessEvaluable;
419
420 type TestBackend = NdArray;
421
422 struct Sphere;
423 struct SphereFit;
424 impl FitnessEvaluable for SphereFit {
425 type Individual = Vec<f64>;
426 type Landscape = Sphere;
427 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
428 x.iter().map(|v| v * v).sum()
429 }
430 }
431
432 #[test]
433 fn abc_converges_on_sphere_d10() {
434 let device = Default::default();
435 let strategy = ArtificialBeeColony::<TestBackend>::new();
436 let params = AbcConfig::default_for(30, 10);
437 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
438 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
439 strategy, params, fitness_fn, 13, device, 400,
440 );
441 harness.reset();
442 while !harness.step(()).done {}
443 let best = harness.latest_metrics().unwrap().best_fitness_ever;
444 assert!(best < 1e-4, "ABC D10 best={best}");
445 }
446}