rlevo_evolution/algorithms/metaheuristic/
aco_r.rs1use std::f32::consts::PI;
21use std::marker::PhantomData;
22
23use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
24use rand::Rng;
25use rand::RngExt;
26use rand_distr::{Distribution as RandDistDist, Normal};
27
28use crate::rng::{SeedPurpose, seed_stream};
29use crate::strategy::{Strategy, StrategyMetrics};
30
31#[derive(Debug, Clone)]
33pub struct AcoRConfig {
34 pub archive_size: usize,
37 pub m: usize,
39 pub genome_dim: usize,
41 pub bounds: (f32, f32),
43 pub xi: f32,
45 pub q: f32,
48}
49
50impl AcoRConfig {
51 #[must_use]
53 pub fn default_for(archive_size: usize, m: usize, genome_dim: usize) -> Self {
54 Self {
55 archive_size,
56 m,
57 genome_dim,
58 bounds: (-5.12, 5.12),
59 xi: 0.85,
60 q: 0.1,
61 }
62 }
63
64 #[must_use]
68 pub fn steady_state_pop_size(&self) -> usize {
69 self.m
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct AcoRState<B: Backend> {
76 pub archive: Tensor<B, 2>,
78 pub archive_fitness: Vec<f32>,
81 pub weights: Vec<f32>,
83 pub best_genome: Option<Tensor<B, 2>>,
85 pub best_fitness: f32,
87 pub generation: usize,
89}
90
91#[derive(Debug, Clone, Copy, Default)]
110pub struct AntColonyReal<B: Backend> {
111 _backend: PhantomData<fn() -> B>,
112}
113
114impl<B: Backend> AntColonyReal<B> {
115 #[must_use]
117 pub fn new() -> Self {
118 Self {
119 _backend: PhantomData,
120 }
121 }
122
123 fn compute_weights(archive_size: usize, q: f32) -> Vec<f32> {
125 #[allow(clippy::cast_precision_loss)]
126 let k = archive_size as f32;
127 let denom = 2.0 * q * q * k * k;
128 let scale = 1.0 / (q * k * (2.0 * PI).sqrt());
129 let mut w: Vec<f32> = (0..archive_size)
130 .map(|l| {
131 #[allow(clippy::cast_precision_loss)]
132 let rank = l as f32;
133 scale * (-(rank * rank) / denom).exp()
134 })
135 .collect();
136 let total: f32 = w.iter().sum();
137 for v in &mut w {
138 *v /= total;
139 }
140 w
141 }
142}
143
144impl<B: Backend> Strategy<B> for AntColonyReal<B>
145where
146 B::Device: Clone,
147{
148 type Params = AcoRConfig;
149 type State = AcoRState<B>;
150 type Genome = Tensor<B, 2>;
151
152 fn init(&self, params: &AcoRConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> AcoRState<B> {
165 assert!(params.archive_size >= 2, "ACO_R requires archive_size >= 2");
166 assert!(params.m >= 1, "ACO_R requires m >= 1");
167 let (lo, hi) = params.bounds;
168 let rows = params.archive_size;
173 let genome_dim = params.genome_dim;
174 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
175 let mut archive_rows = Vec::with_capacity(rows * genome_dim);
176 for _ in 0..rows * genome_dim {
177 archive_rows.push(lo + (hi - lo) * stream.random::<f32>());
178 }
179 let archive =
180 Tensor::<B, 2>::from_data(TensorData::new(archive_rows, [rows, genome_dim]), device);
181 AcoRState {
182 archive,
183 archive_fitness: Vec::new(),
184 weights: Self::compute_weights(params.archive_size, params.q),
185 best_genome: None,
186 best_fitness: f32::INFINITY,
187 generation: 0,
188 }
189 }
190
191 #[allow(clippy::many_single_char_names)]
211 fn ask(
212 &self,
213 params: &AcoRConfig,
214 state: &AcoRState<B>,
215 rng: &mut dyn Rng,
216 device: &<B as burn::tensor::backend::BackendTypes>::Device,
217 ) -> (Tensor<B, 2>, AcoRState<B>) {
218 if state.archive_fitness.is_empty() {
220 return (state.archive.clone(), state.clone());
221 }
222
223 let k = params.archive_size;
224 let m = params.m;
225 let d = params.genome_dim;
226
227 let archive_l = state.archive.clone().unsqueeze_dim::<3>(0); let archive_e = state.archive.clone().unsqueeze_dim::<3>(1); let diffs = (archive_l.expand([k, k, d]) - archive_e.expand([k, k, d])).abs();
233 #[allow(clippy::cast_precision_loss)]
234 let inv = params.xi / ((k - 1).max(1) as f32);
235 let sigma = diffs.sum_dim(0).squeeze::<2>().mul_scalar(inv); let mut stream = seed_stream(
239 rng.next_u64(),
240 state.generation as u64,
241 SeedPurpose::Selection,
242 );
243 let mut mean_rows = vec![0f32; m * d];
244 let mut sigma_rows = vec![0f32; m * d];
245
246 let archive_host = state.archive.clone().into_data().into_vec::<f32>().unwrap();
248 let sigma_host = sigma.into_data().into_vec::<f32>().unwrap();
249 let cdf: Vec<f32> = {
250 let mut acc = 0.0;
251 let mut v = Vec::with_capacity(k);
252 for &w in &state.weights {
253 acc += w;
254 v.push(acc);
255 }
256 v
257 };
258 let pick = |u: f32| -> usize { cdf.iter().position(|&c| u <= c).unwrap_or(k - 1) };
259
260 for i in 0..m {
261 for j in 0..d {
262 let u: f32 = stream.random::<f32>();
263 let l = pick(u);
264 mean_rows[i * d + j] = archive_host[l * d + j];
265 sigma_rows[i * d + j] = sigma_host[l * d + j].max(1e-12);
266 }
267 }
268
269 let mut offspring = vec![0f32; m * d];
273 let mut sample_rng = seed_stream(
274 rng.next_u64(),
275 state.generation as u64,
276 SeedPurpose::Mutation,
277 );
278 for (idx, out) in offspring.iter_mut().enumerate() {
279 let normal = Normal::new(mean_rows[idx], sigma_rows[idx]).expect("sigma > 0");
280 *out = normal.sample(&mut sample_rng);
281 }
282 let (lo, hi) = params.bounds;
283 for v in &mut offspring {
284 *v = v.clamp(lo, hi);
285 }
286 let new_pop = Tensor::<B, 2>::from_data(TensorData::new(offspring, [m, d]), device);
287
288 (new_pop, state.clone())
289 }
290
291 fn tell(
305 &self,
306 params: &AcoRConfig,
307 population: Tensor<B, 2>,
308 fitness: Tensor<B, 1>,
309 mut state: AcoRState<B>,
310 _rng: &mut dyn Rng,
311 ) -> (AcoRState<B>, StrategyMetrics) {
312 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
313 let device = population.device();
314 let k = params.archive_size;
315
316 if state.archive_fitness.is_empty() {
318 let mut idx: Vec<usize> = (0..fitness_host.len()).collect();
320 idx.sort_by(|&a, &b| fitness_host[a].partial_cmp(&fitness_host[b]).unwrap());
321 #[allow(clippy::cast_possible_wrap)]
322 let sorted_idx = Tensor::<B, 1, Int>::from_data(
323 TensorData::new(idx.iter().map(|&i| i as i64).collect::<Vec<_>>(), [k]),
324 &device,
325 );
326 state.archive = population.clone().select(0, sorted_idx);
327 state.archive_fitness = idx.iter().map(|&i| fitness_host[i]).collect();
328 state.best_fitness = state.archive_fitness[0];
329 let first_idx =
330 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![0_i64], [1]), &device);
331 state.best_genome = Some(state.archive.clone().select(0, first_idx));
332 state.generation += 1;
333 let m = StrategyMetrics::from_host_fitness(
334 state.generation,
335 &fitness_host,
336 state.best_fitness,
337 );
338 state.best_fitness = m.best_fitness_ever;
339 return (state, m);
340 }
341
342 let combined = Tensor::cat(vec![state.archive.clone(), population.clone()], 0);
344 let mut combined_f: Vec<f32> = state.archive_fitness.clone();
345 combined_f.extend_from_slice(&fitness_host);
346 let mut idx: Vec<usize> = (0..combined_f.len()).collect();
347 idx.sort_by(|&a, &b| combined_f[a].partial_cmp(&combined_f[b]).unwrap());
348 idx.truncate(k);
349 #[allow(clippy::cast_possible_wrap)]
350 let top_idx = Tensor::<B, 1, Int>::from_data(
351 TensorData::new(idx.iter().map(|&i| i as i64).collect::<Vec<_>>(), [k]),
352 &device,
353 );
354 state.archive = combined.select(0, top_idx);
355 state.archive_fitness = idx.iter().map(|&i| combined_f[i]).collect();
356
357 if state.archive_fitness[0] < state.best_fitness {
358 state.best_fitness = state.archive_fitness[0];
359 let first_idx =
360 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![0_i64], [1]), &device);
361 state.best_genome = Some(state.archive.clone().select(0, first_idx));
362 }
363
364 state.generation += 1;
365 let m =
366 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
367 state.best_fitness = m.best_fitness_ever;
368 (state, m)
369 }
370
371 fn best(&self, state: &AcoRState<B>) -> Option<(Tensor<B, 2>, f32)> {
376 state
377 .best_genome
378 .as_ref()
379 .map(|g| (g.clone(), state.best_fitness))
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use crate::fitness::FromFitnessEvaluable;
387 use crate::strategy::EvolutionaryHarness;
388 use burn::backend::Flex;
389 use rlevo_core::fitness::FitnessEvaluable;
390
391 type TestBackend = Flex;
392
393 struct Sphere;
394 struct SphereFit;
395 impl FitnessEvaluable for SphereFit {
396 type Individual = Vec<f64>;
397 type Landscape = Sphere;
398 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
399 x.iter().map(|v| v * v).sum()
400 }
401 }
402
403 #[test]
404 fn weights_sum_to_one() {
405 let w = AntColonyReal::<TestBackend>::compute_weights(10, 0.1);
406 let total: f32 = w.iter().sum();
407 approx::assert_relative_eq!(total, 1.0, epsilon = 1e-5);
408 }
409
410 #[test]
411 fn aco_r_converges_on_sphere_d10() {
412 let device = Default::default();
413 let strategy = AntColonyReal::<TestBackend>::new();
414 let params = AcoRConfig::default_for(30, 15, 10);
415 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
416 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
417 strategy, params, fitness_fn, 17, device, 400,
418 );
419 harness.reset();
420 while !harness.step(()).done {}
421 let best = harness.latest_metrics().unwrap().best_fitness_ever;
422 assert!(best < 1e-3, "ACO_R D10 best={best}");
423 }
424}