rlevo_evolution/algorithms/metaheuristic/
salp.rs1use std::marker::PhantomData;
35
36use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
37use rand::Rng;
38use rand::RngExt;
39
40use crate::rng::{SeedPurpose, seed_stream};
41use crate::strategy::{Strategy, StrategyMetrics};
42
43#[derive(Debug, Clone)]
45pub struct SalpConfig {
46 pub pop_size: usize,
49 pub genome_dim: usize,
51 pub bounds: (f32, f32),
54 pub max_generations: usize,
56}
57
58impl SalpConfig {
59 #[must_use]
61 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
62 Self {
63 pop_size,
64 genome_dim,
65 bounds: (-5.12, 5.12),
66 max_generations: 500,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct SalpState<B: Backend> {
74 pub positions: Tensor<B, 2>,
76 pub fitness: Vec<f32>,
78 pub best_genome: Option<Tensor<B, 2>>,
80 pub best_fitness: f32,
82 pub generation: usize,
84}
85
86#[derive(Debug, Clone, Copy, Default)]
110pub struct SalpSwarm<B: Backend> {
111 _backend: PhantomData<fn() -> B>,
112}
113
114impl<B: Backend> SalpSwarm<B> {
115 #[must_use]
117 pub fn new() -> Self {
118 Self {
119 _backend: PhantomData,
120 }
121 }
122}
123
124impl<B: Backend> Strategy<B> for SalpSwarm<B>
125where
126 B::Device: Clone,
127{
128 type Params = SalpConfig;
129 type State = SalpState<B>;
130 type Genome = Tensor<B, 2>;
131
132 fn init(&self, params: &SalpConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> SalpState<B> {
139 assert!(params.pop_size >= 2, "SSA requires pop_size >= 2");
140 let (lo, hi) = params.bounds;
141 let pop = params.pop_size;
146 let genome_dim = params.genome_dim;
147 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
148 let mut position_rows = Vec::with_capacity(pop * genome_dim);
149 for _ in 0..pop * genome_dim {
150 position_rows.push(lo + (hi - lo) * stream.random::<f32>());
151 }
152 let positions =
153 Tensor::<B, 2>::from_data(TensorData::new(position_rows, [pop, genome_dim]), device);
154 SalpState {
155 positions,
156 fitness: Vec::new(),
157 best_genome: None,
158 best_fitness: f32::INFINITY,
159 generation: 0,
160 }
161 }
162
163 fn ask(
177 &self,
178 params: &SalpConfig,
179 state: &SalpState<B>,
180 rng: &mut dyn Rng,
181 device: &<B as burn::tensor::backend::BackendTypes>::Device,
182 ) -> (Tensor<B, 2>, SalpState<B>) {
183 if state.fitness.is_empty() {
184 return (state.positions.clone(), state.clone());
185 }
186
187 let pop_size = params.pop_size;
188 let genome_dim = params.genome_dim;
189 let n_leaders = pop_size / 2;
190 let (lo, hi) = params.bounds;
191
192 #[allow(clippy::cast_precision_loss)]
194 let t = state.generation as f32;
195 #[allow(clippy::cast_precision_loss)]
196 let max_t = params.max_generations.max(1) as f32;
197 let frac = (4.0 * t / max_t).min(4.0);
198 let c1 = 2.0 * (-(frac * frac)).exp();
199
200 let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
203 let mut leader_delta: Vec<f32> = Vec::with_capacity(n_leaders * genome_dim);
204 for _ in 0..n_leaders {
205 for _ in 0..genome_dim {
206 let c2: f32 = stream.random::<f32>();
207 let c3: f32 = stream.random::<f32>();
208 let scaled = (hi - lo) * c2 + lo;
209 let sign = if c3 >= 0.5 { 1.0 } else { -1.0 };
210 leader_delta.push(sign * c1 * scaled);
211 }
212 }
213
214 let best = state
215 .best_genome
216 .as_ref()
217 .expect("best_genome populated after first tell")
218 .clone()
219 .expand([n_leaders, genome_dim]);
220 let delta = Tensor::<B, 2>::from_data(
221 TensorData::new(leader_delta, [n_leaders, genome_dim]),
222 device,
223 );
224 let new_leaders = (best + delta).clamp(lo, hi);
225
226 let followers = state
228 .positions
229 .clone()
230 .slice([n_leaders..pop_size, 0..genome_dim]);
231 let joined = Tensor::cat(vec![new_leaders.clone(), followers.clone()], 0); #[allow(clippy::cast_possible_wrap)]
238 let shift_idx: Vec<i64> = (0..(pop_size - n_leaders))
239 .map(|k| (n_leaders + k - 1) as i64)
240 .collect();
241 let idx = Tensor::<B, 1, Int>::from_data(
242 TensorData::new(shift_idx, [pop_size - n_leaders]),
243 device,
244 );
245 let previous = joined.clone().select(0, idx);
246 let new_followers = (followers + previous).mul_scalar(0.5).clamp(lo, hi);
247
248 let new_positions = Tensor::cat(vec![new_leaders, new_followers], 0);
249 let mut next = state.clone();
250 next.positions.clone_from(&new_positions);
251 (new_positions, next)
252 }
253
254 fn tell(
260 &self,
261 _params: &SalpConfig,
262 population: Tensor<B, 2>,
263 fitness: Tensor<B, 1>,
264 mut state: SalpState<B>,
265 _rng: &mut dyn Rng,
266 ) -> (SalpState<B>, StrategyMetrics) {
267 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
268 state.fitness.clone_from(&fitness_host);
269 state.positions.clone_from(&population);
270 let best_idx = argmin(&fitness_host);
271 if fitness_host[best_idx] < state.best_fitness {
272 state.best_fitness = fitness_host[best_idx];
273 let device = population.device();
274 #[allow(clippy::cast_possible_wrap)]
275 let idx = Tensor::<B, 1, Int>::from_data(
276 TensorData::new(vec![best_idx as i64], [1]),
277 &device,
278 );
279 state.best_genome = Some(population.select(0, idx));
280 }
281 state.generation += 1;
282 let m =
283 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
284 state.best_fitness = m.best_fitness_ever;
285 (state, m)
286 }
287
288 fn best(&self, state: &SalpState<B>) -> Option<(Tensor<B, 2>, f32)> {
291 state
292 .best_genome
293 .as_ref()
294 .map(|g| (g.clone(), state.best_fitness))
295 }
296}
297
298fn argmin(xs: &[f32]) -> usize {
299 let mut best_idx = 0usize;
300 let mut best = f32::INFINITY;
301 for (i, &v) in xs.iter().enumerate() {
302 if v < best {
303 best = v;
304 best_idx = i;
305 }
306 }
307 best_idx
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::fitness::FromFitnessEvaluable;
314 use crate::strategy::EvolutionaryHarness;
315 use burn::backend::Flex;
316 use rlevo_core::fitness::FitnessEvaluable;
317
318 type TestBackend = Flex;
319
320 struct Sphere;
321 struct SphereFit;
322 impl FitnessEvaluable for SphereFit {
323 type Individual = Vec<f64>;
324 type Landscape = Sphere;
325 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
326 x.iter().map(|v| v * v).sum()
327 }
328 }
329
330 #[test]
331 fn ssa_converges_on_sphere_d10() {
332 let device = Default::default();
337 let strategy = SalpSwarm::<TestBackend>::new();
338 let params = SalpConfig::default_for(40, 10);
339 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
340 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
341 strategy, params, fitness_fn, 3, device, 600,
342 );
343 harness.reset();
344 while !harness.step(()).done {}
345 let best = harness.latest_metrics().unwrap().best_fitness_ever;
346 assert!(best < 1e-2, "SSA D10 best={best}");
347 }
348}