1use std::marker::PhantomData;
37
38use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
39use rand::Rng;
40use rand::RngExt;
41
42use crate::rng::{SeedPurpose, seed_stream};
43use crate::strategy::{Strategy, StrategyMetrics};
44
45#[derive(Debug, Clone)]
47pub struct AbcConfig {
48 pub pop_size: usize,
51 pub genome_dim: usize,
53 pub bounds: (f32, f32),
55 pub limit: usize,
58 pub tournament_size: usize,
62}
63
64impl AbcConfig {
65 #[must_use]
71 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
72 Self {
73 pop_size,
74 genome_dim,
75 bounds: (-5.12, 5.12),
76 limit: (pop_size * genome_dim) / 2,
77 tournament_size: 3,
78 }
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct AbcState<B: Backend> {
85 pub colony: Tensor<B, 2>,
87 pub fitness: Vec<f32>,
89 pub trial: Vec<usize>,
91 pub target_of_candidate: Vec<usize>,
96 pub best_genome: Option<Tensor<B, 2>>,
98 pub best_fitness: f32,
100 pub generation: usize,
102}
103
104#[derive(Debug, Clone, Copy, Default)]
123pub struct ArtificialBeeColony<B: Backend> {
124 _backend: PhantomData<fn() -> B>,
125}
126
127impl<B: Backend> ArtificialBeeColony<B> {
128 #[must_use]
130 pub fn new() -> Self {
131 Self {
132 _backend: PhantomData,
133 }
134 }
135
136 #[allow(clippy::too_many_arguments)]
137 fn build_candidates(
138 targets: &[usize],
139 neighbors: &[usize],
140 dims: &[usize],
141 phi: &[f32],
142 colony: &Tensor<B, 2>,
143 pop_size: usize,
144 genome_dim: usize,
145 device: &<B as burn::tensor::backend::BackendTypes>::Device,
146 ) -> Tensor<B, 2> {
147 #[allow(clippy::cast_possible_wrap)]
149 let target_idx: Vec<i64> = targets.iter().map(|&i| i as i64).collect();
150 let _ = pop_size; let n_cand = targets.len();
152 let target_tensor =
153 Tensor::<B, 1, Int>::from_data(TensorData::new(target_idx, [n_cand]), device);
154 let base = colony.clone().select(0, target_tensor);
155
156 #[allow(clippy::cast_possible_wrap)]
158 let neighbor_idx: Vec<i64> = neighbors.iter().map(|&i| i as i64).collect();
159 let neighbor_tensor =
160 Tensor::<B, 1, Int>::from_data(TensorData::new(neighbor_idx, [n_cand]), device);
161 let neighbor_rows = colony.clone().select(0, neighbor_tensor);
162
163 let mut mask = vec![0i64; n_cand * genome_dim];
165 for (row, &j) in dims.iter().enumerate() {
166 mask[row * genome_dim + j] = 1;
167 }
168 let mask_bool =
169 Tensor::<B, 2, Int>::from_data(TensorData::new(mask, [n_cand, genome_dim]), device)
170 .equal_elem(1);
171
172 let phi_row = Tensor::<B, 1>::from_data(TensorData::new(phi.to_vec(), [n_cand]), device)
174 .unsqueeze_dim::<2>(1)
175 .expand([n_cand, genome_dim]);
176 let delta = phi_row.mul(base.clone() - neighbor_rows);
177 let perturbed = base.clone() + delta;
178 base.mask_where(mask_bool, perturbed)
179 }
180}
181
182impl<B: Backend> Strategy<B> for ArtificialBeeColony<B>
183where
184 B::Device: Clone,
185{
186 type Params = AbcConfig;
187 type State = AbcState<B>;
188 type Genome = Tensor<B, 2>;
189
190 fn init(&self, params: &AbcConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> AbcState<B> {
191 assert!(params.pop_size >= 2, "ABC requires pop_size >= 2");
192 let (lo, hi) = params.bounds;
193 let pop = params.pop_size;
198 let genome_dim = params.genome_dim;
199 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
200 let mut colony_rows = Vec::with_capacity(pop * genome_dim);
201 for _ in 0..pop * genome_dim {
202 colony_rows.push(lo + (hi - lo) * stream.random::<f32>());
203 }
204 let colony =
205 Tensor::<B, 2>::from_data(TensorData::new(colony_rows, [pop, genome_dim]), device);
206 AbcState {
207 colony,
208 fitness: Vec::new(),
209 trial: vec![0; params.pop_size],
210 target_of_candidate: Vec::new(),
211 best_genome: None,
212 best_fitness: f32::INFINITY,
213 generation: 0,
214 }
215 }
216
217 fn ask(
218 &self,
219 params: &AbcConfig,
220 state: &AbcState<B>,
221 rng: &mut dyn Rng,
222 device: &<B as burn::tensor::backend::BackendTypes>::Device,
223 ) -> (Tensor<B, 2>, AbcState<B>) {
224 if state.fitness.is_empty() {
225 return (state.colony.clone(), state.clone());
226 }
227
228 let pop = params.pop_size;
229 let genome_dim = params.genome_dim;
230 let n_cand = 2 * pop;
231
232 let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
233
234 let mut targets = Vec::with_capacity(n_cand);
235 let mut neighbors = Vec::with_capacity(n_cand);
236 let mut dims = Vec::with_capacity(n_cand);
237 let mut phis = Vec::with_capacity(n_cand);
238
239 for i in 0..pop {
241 targets.push(i);
242 }
243 for _ in 0..pop {
245 let mut best = stream.random_range(0..pop);
246 for _ in 1..params.tournament_size {
247 let c = stream.random_range(0..pop);
248 if state.fitness[c] < state.fitness[best] {
249 best = c;
250 }
251 }
252 targets.push(best);
253 }
254 for &t in &targets {
256 let mut k = stream.random_range(0..pop);
257 if k == t {
258 k = (k + 1) % pop;
259 }
260 neighbors.push(k);
261 dims.push(stream.random_range(0..genome_dim));
262 let phi = 2.0 * stream.random::<f32>() - 1.0;
263 phis.push(phi);
264 }
265
266 let candidates = Self::build_candidates(
267 &targets,
268 &neighbors,
269 &dims,
270 &phis,
271 &state.colony,
272 pop,
273 genome_dim,
274 device,
275 );
276 let (lo, hi) = params.bounds;
277 let candidates = candidates.clamp(lo, hi);
278
279 let mut next = state.clone();
280 next.target_of_candidate = targets;
281 (candidates, next)
282 }
283
284 #[allow(clippy::too_many_lines)]
285 fn tell(
286 &self,
287 params: &AbcConfig,
288 candidates: Tensor<B, 2>,
289 fitness: Tensor<B, 1>,
290 mut state: AbcState<B>,
291 rng: &mut dyn Rng,
292 ) -> (AbcState<B>, StrategyMetrics) {
293 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
294 let device = candidates.device();
295 let pop = params.pop_size;
296 let genome_dim = params.genome_dim;
297
298 if state.fitness.is_empty() {
300 state.fitness.clone_from(&fitness_host);
301 let best_idx = argmin(&fitness_host);
302 state.best_fitness = fitness_host[best_idx];
303 #[allow(clippy::cast_possible_wrap)]
304 let idx = Tensor::<B, 1, Int>::from_data(
305 TensorData::new(vec![best_idx as i64], [1]),
306 &device,
307 );
308 state.best_genome = Some(candidates.clone().select(0, idx));
309 state.colony = candidates;
310 state.generation += 1;
311 let m = StrategyMetrics::from_host_fitness(
312 state.generation,
313 &fitness_host,
314 state.best_fitness,
315 );
316 state.best_fitness = m.best_fitness_ever;
317 return (state, m);
318 }
319
320 let mut best_per_target: Vec<Option<(usize, f32)>> = vec![None; pop];
323 for (cand_idx, &t) in state.target_of_candidate.iter().enumerate() {
324 let cand_fit = fitness_host[cand_idx];
325 if cand_fit <= state.fitness[t] {
326 match best_per_target[t] {
327 None => best_per_target[t] = Some((cand_idx, cand_fit)),
328 Some((_, prev)) if cand_fit < prev => {
329 best_per_target[t] = Some((cand_idx, cand_fit));
330 }
331 _ => {}
332 }
333 }
334 }
335
336 let stacked = Tensor::cat(vec![state.colony.clone(), candidates.clone()], 0);
341 #[allow(clippy::cast_possible_wrap)]
342 let mut rs: Vec<i64> = (0..pop).map(|i| i as i64).collect();
343 let mut new_fitness = state.fitness.clone();
344 for t in 0..pop {
345 match best_per_target[t] {
346 Some((cand_idx, cand_fit)) => {
347 #[allow(clippy::cast_possible_wrap)]
348 {
349 rs[t] = (pop + cand_idx) as i64;
350 }
351 new_fitness[t] = cand_fit;
352 state.trial[t] = 0;
353 }
354 None => {
355 state.trial[t] += 1;
356 }
357 }
358 }
359 let idx = Tensor::<B, 1, Int>::from_data(TensorData::new(rs, [pop]), &device);
360 state.colony = stacked.select(0, idx);
361 state.fitness = new_fitness;
362
363 let mut scouts: Vec<usize> = Vec::new();
365 for (i, trial) in state.trial.iter_mut().enumerate() {
366 if *trial > params.limit {
367 scouts.push(i);
368 *trial = 0;
369 }
370 }
371 if !scouts.is_empty() {
372 let (lo, hi) = params.bounds;
373 let mut scout_stream =
377 seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Replacement);
378 let mut fresh_rows = Vec::with_capacity(scouts.len() * genome_dim);
379 for _ in 0..scouts.len() * genome_dim {
380 fresh_rows.push(lo + (hi - lo) * scout_stream.random::<f32>());
381 }
382 let fresh = Tensor::<B, 2>::from_data(
383 TensorData::new(fresh_rows, [scouts.len(), genome_dim]),
384 &device,
385 );
386 #[allow(clippy::cast_possible_wrap)]
388 let mut rs2: Vec<i64> = (0..pop).map(|i| i as i64).collect();
389 for (k, &scout) in scouts.iter().enumerate() {
390 #[allow(clippy::cast_possible_wrap)]
391 {
392 rs2[scout] = (pop + k) as i64;
393 }
394 state.fitness[scout] = f32::INFINITY;
397 }
398 let stacked2 = Tensor::cat(vec![state.colony.clone(), fresh], 0);
399 let idx2 = Tensor::<B, 1, Int>::from_data(TensorData::new(rs2, [pop]), &device);
400 state.colony = stacked2.select(0, idx2);
401 }
402
403 let best_idx = argmin(&state.fitness);
406 if state.fitness[best_idx].is_finite() && state.fitness[best_idx] < state.best_fitness {
407 state.best_fitness = state.fitness[best_idx];
408 #[allow(clippy::cast_possible_wrap)]
409 let idx = Tensor::<B, 1, Int>::from_data(
410 TensorData::new(vec![best_idx as i64], [1]),
411 &device,
412 );
413 state.best_genome = Some(state.colony.clone().select(0, idx));
414 }
415
416 state.generation += 1;
417 let m =
418 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
419 state.best_fitness = m.best_fitness_ever;
420 (state, m)
421 }
422
423 fn best(&self, state: &AbcState<B>) -> Option<(Tensor<B, 2>, f32)> {
424 state
425 .best_genome
426 .as_ref()
427 .map(|g| (g.clone(), state.best_fitness))
428 }
429}
430
431fn argmin(xs: &[f32]) -> usize {
432 let mut best_idx = 0usize;
433 let mut best = f32::INFINITY;
434 for (i, &v) in xs.iter().enumerate() {
435 if v < best {
436 best = v;
437 best_idx = i;
438 }
439 }
440 best_idx
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::fitness::FromFitnessEvaluable;
447 use crate::strategy::EvolutionaryHarness;
448 use burn::backend::Flex;
449 use rlevo_core::fitness::FitnessEvaluable;
450
451 type TestBackend = Flex;
452
453 struct Sphere;
454 struct SphereFit;
455 impl FitnessEvaluable for SphereFit {
456 type Individual = Vec<f64>;
457 type Landscape = Sphere;
458 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
459 x.iter().map(|v| v * v).sum()
460 }
461 }
462
463 #[test]
464 fn abc_converges_on_sphere_d10() {
465 let device = Default::default();
466 let strategy = ArtificialBeeColony::<TestBackend>::new();
467 let params = AbcConfig::default_for(30, 10);
468 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
469 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
470 strategy, params, fitness_fn, 13, device, 400,
471 );
472 harness.reset();
473 while !harness.step(()).done {}
474 let best = harness.latest_metrics().unwrap().best_fitness_ever;
475 assert!(best < 1e-4, "ABC D10 best={best}");
476 }
477}