rlevo_evolution/algorithms/metaheuristic/
aco_r.rs1use std::f32::consts::PI;
21use std::marker::PhantomData;
22
23use burn::tensor::{Distribution, Int, Tensor, TensorData, backend::Backend};
24use rand::Rng;
25use rand_distr::{Distribution as RandDistDist, Normal};
26
27use crate::rng::{SeedPurpose, seed_stream};
28use crate::strategy::{Strategy, StrategyMetrics};
29
30#[derive(Debug, Clone)]
32pub struct AcoRConfig {
33 pub archive_size: usize,
36 pub m: usize,
38 pub genome_dim: usize,
40 pub bounds: (f32, f32),
42 pub xi: f32,
44 pub q: f32,
47}
48
49impl AcoRConfig {
50 #[must_use]
52 pub fn default_for(archive_size: usize, m: usize, genome_dim: usize) -> Self {
53 Self {
54 archive_size,
55 m,
56 genome_dim,
57 bounds: (-5.12, 5.12),
58 xi: 0.85,
59 q: 0.1,
60 }
61 }
62
63 #[must_use]
67 pub fn steady_state_pop_size(&self) -> usize {
68 self.m
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct AcoRState<B: Backend> {
75 pub archive: Tensor<B, 2>,
77 pub archive_fitness: Vec<f32>,
80 pub weights: Vec<f32>,
82 pub best_genome: Option<Tensor<B, 2>>,
84 pub best_fitness: f32,
86 pub generation: usize,
88}
89
90#[derive(Debug, Clone, Copy, Default)]
109pub struct AntColonyReal<B: Backend> {
110 _backend: PhantomData<fn() -> B>,
111}
112
113impl<B: Backend> AntColonyReal<B> {
114 #[must_use]
116 pub fn new() -> Self {
117 Self {
118 _backend: PhantomData,
119 }
120 }
121
122 fn compute_weights(archive_size: usize, q: f32) -> Vec<f32> {
124 #[allow(clippy::cast_precision_loss)]
125 let k = archive_size as f32;
126 let denom = 2.0 * q * q * k * k;
127 let scale = 1.0 / (q * k * (2.0 * PI).sqrt());
128 let mut w: Vec<f32> = (0..archive_size)
129 .map(|l| {
130 #[allow(clippy::cast_precision_loss)]
131 let rank = l as f32;
132 scale * (-(rank * rank) / denom).exp()
133 })
134 .collect();
135 let total: f32 = w.iter().sum();
136 for v in &mut w {
137 *v /= total;
138 }
139 w
140 }
141}
142
143impl<B: Backend> Strategy<B> for AntColonyReal<B>
144where
145 B::Device: Clone,
146{
147 type Params = AcoRConfig;
148 type State = AcoRState<B>;
149 type Genome = Tensor<B, 2>;
150
151 fn init(&self, params: &AcoRConfig, rng: &mut dyn Rng, device: &B::Device) -> AcoRState<B> {
152 assert!(params.archive_size >= 2, "ACO_R requires archive_size >= 2");
153 assert!(params.m >= 1, "ACO_R requires m >= 1");
154 let (lo, hi) = params.bounds;
155 B::seed(device, rng.next_u64());
156 let archive = Tensor::<B, 2>::random(
157 [params.archive_size, params.genome_dim],
158 Distribution::Uniform(f64::from(lo), f64::from(hi)),
159 device,
160 );
161 AcoRState {
162 archive,
163 archive_fitness: Vec::new(),
164 weights: Self::compute_weights(params.archive_size, params.q),
165 best_genome: None,
166 best_fitness: f32::INFINITY,
167 generation: 0,
168 }
169 }
170
171 #[allow(clippy::many_single_char_names)]
172 fn ask(
173 &self,
174 params: &AcoRConfig,
175 state: &AcoRState<B>,
176 rng: &mut dyn Rng,
177 device: &B::Device,
178 ) -> (Tensor<B, 2>, AcoRState<B>) {
179 if state.archive_fitness.is_empty() {
181 return (state.archive.clone(), state.clone());
182 }
183
184 let k = params.archive_size;
185 let m = params.m;
186 let d = params.genome_dim;
187
188 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();
194 #[allow(clippy::cast_precision_loss)]
195 let inv = params.xi / ((k - 1).max(1) as f32);
196 let sigma = diffs.sum_dim(0).squeeze::<2>().mul_scalar(inv); let mut stream = seed_stream(
200 rng.next_u64(),
201 state.generation as u64,
202 SeedPurpose::Selection,
203 );
204 let mut mean_rows = vec![0f32; m * d];
205 let mut sigma_rows = vec![0f32; m * d];
206
207 let archive_host = state.archive.clone().into_data().into_vec::<f32>().unwrap();
209 let sigma_host = sigma.into_data().into_vec::<f32>().unwrap();
210 let cdf: Vec<f32> = {
211 let mut acc = 0.0;
212 let mut v = Vec::with_capacity(k);
213 for &w in &state.weights {
214 acc += w;
215 v.push(acc);
216 }
217 v
218 };
219 let pick = |u: f32| -> usize { cdf.iter().position(|&c| u <= c).unwrap_or(k - 1) };
220
221 for i in 0..m {
222 for j in 0..d {
223 use rand::RngExt;
224 let u: f32 = stream.random::<f32>();
225 let l = pick(u);
226 mean_rows[i * d + j] = archive_host[l * d + j];
227 sigma_rows[i * d + j] = sigma_host[l * d + j].max(1e-12);
228 }
229 }
230
231 let mut offspring = vec![0f32; m * d];
235 let mut sample_rng = seed_stream(
236 rng.next_u64(),
237 state.generation as u64,
238 SeedPurpose::Mutation,
239 );
240 for (idx, out) in offspring.iter_mut().enumerate() {
241 let normal = Normal::new(mean_rows[idx], sigma_rows[idx]).expect("sigma > 0");
242 *out = normal.sample(&mut sample_rng);
243 }
244 let (lo, hi) = params.bounds;
245 for v in &mut offspring {
246 *v = v.clamp(lo, hi);
247 }
248 let new_pop = Tensor::<B, 2>::from_data(TensorData::new(offspring, [m, d]), device);
249
250 (new_pop, state.clone())
251 }
252
253 fn tell(
254 &self,
255 params: &AcoRConfig,
256 population: Tensor<B, 2>,
257 fitness: Tensor<B, 1>,
258 mut state: AcoRState<B>,
259 _rng: &mut dyn Rng,
260 ) -> (AcoRState<B>, StrategyMetrics) {
261 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
262 let device = population.device();
263 let k = params.archive_size;
264
265 if state.archive_fitness.is_empty() {
267 let mut idx: Vec<usize> = (0..fitness_host.len()).collect();
269 idx.sort_by(|&a, &b| fitness_host[a].partial_cmp(&fitness_host[b]).unwrap());
270 #[allow(clippy::cast_possible_wrap)]
271 let sorted_idx = Tensor::<B, 1, Int>::from_data(
272 TensorData::new(idx.iter().map(|&i| i as i64).collect::<Vec<_>>(), [k]),
273 &device,
274 );
275 state.archive = population.clone().select(0, sorted_idx);
276 state.archive_fitness = idx.iter().map(|&i| fitness_host[i]).collect();
277 state.best_fitness = state.archive_fitness[0];
278 let first_idx =
279 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![0_i64], [1]), &device);
280 state.best_genome = Some(state.archive.clone().select(0, first_idx));
281 state.generation += 1;
282 let m = StrategyMetrics::from_host_fitness(
283 state.generation,
284 &fitness_host,
285 state.best_fitness,
286 );
287 state.best_fitness = m.best_fitness_ever;
288 return (state, m);
289 }
290
291 let combined = Tensor::cat(vec![state.archive.clone(), population.clone()], 0);
293 let mut combined_f: Vec<f32> = state.archive_fitness.clone();
294 combined_f.extend_from_slice(&fitness_host);
295 let mut idx: Vec<usize> = (0..combined_f.len()).collect();
296 idx.sort_by(|&a, &b| combined_f[a].partial_cmp(&combined_f[b]).unwrap());
297 idx.truncate(k);
298 #[allow(clippy::cast_possible_wrap)]
299 let top_idx = Tensor::<B, 1, Int>::from_data(
300 TensorData::new(idx.iter().map(|&i| i as i64).collect::<Vec<_>>(), [k]),
301 &device,
302 );
303 state.archive = combined.select(0, top_idx);
304 state.archive_fitness = idx.iter().map(|&i| combined_f[i]).collect();
305
306 if state.archive_fitness[0] < state.best_fitness {
307 state.best_fitness = state.archive_fitness[0];
308 let first_idx =
309 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![0_i64], [1]), &device);
310 state.best_genome = Some(state.archive.clone().select(0, first_idx));
311 }
312
313 state.generation += 1;
314 let m =
315 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
316 state.best_fitness = m.best_fitness_ever;
317 (state, m)
318 }
319
320 fn best(&self, state: &AcoRState<B>) -> Option<(Tensor<B, 2>, f32)> {
321 state
322 .best_genome
323 .as_ref()
324 .map(|g| (g.clone(), state.best_fitness))
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::fitness::FromFitnessEvaluable;
332 use crate::strategy::EvolutionaryHarness;
333 use burn::backend::NdArray;
334 use rlevo_core::fitness::FitnessEvaluable;
335
336 type TestBackend = NdArray;
337
338 struct Sphere;
339 struct SphereFit;
340 impl FitnessEvaluable for SphereFit {
341 type Individual = Vec<f64>;
342 type Landscape = Sphere;
343 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
344 x.iter().map(|v| v * v).sum()
345 }
346 }
347
348 #[test]
349 fn weights_sum_to_one() {
350 let w = AntColonyReal::<TestBackend>::compute_weights(10, 0.1);
351 let total: f32 = w.iter().sum();
352 approx::assert_relative_eq!(total, 1.0, epsilon = 1e-5);
353 }
354
355 #[test]
356 fn aco_r_converges_on_sphere_d10() {
357 let device = Default::default();
358 let strategy = AntColonyReal::<TestBackend>::new();
359 let params = AcoRConfig::default_for(30, 15, 10);
360 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
361 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
362 strategy, params, fitness_fn, 17, device, 400,
363 );
364 harness.reset();
365 while !harness.step(()).done {}
366 let best = harness.latest_metrics().unwrap().best_fitness_ever;
367 assert!(best < 1e-3, "ACO_R D10 best={best}");
368 }
369}