rlevo_evolution/algorithms/metaheuristic/
salp.rs1use std::marker::PhantomData;
35
36use burn::tensor::{Distribution, 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)]
104pub struct SalpSwarm<B: Backend> {
105 _backend: PhantomData<fn() -> B>,
106}
107
108impl<B: Backend> SalpSwarm<B> {
109 #[must_use]
111 pub fn new() -> Self {
112 Self {
113 _backend: PhantomData,
114 }
115 }
116}
117
118impl<B: Backend> Strategy<B> for SalpSwarm<B>
119where
120 B::Device: Clone,
121{
122 type Params = SalpConfig;
123 type State = SalpState<B>;
124 type Genome = Tensor<B, 2>;
125
126 fn init(&self, params: &SalpConfig, rng: &mut dyn Rng, device: &B::Device) -> SalpState<B> {
127 assert!(params.pop_size >= 2, "SSA requires pop_size >= 2");
128 let (lo, hi) = params.bounds;
129 B::seed(device, rng.next_u64());
130 let positions = Tensor::<B, 2>::random(
131 [params.pop_size, params.genome_dim],
132 Distribution::Uniform(f64::from(lo), f64::from(hi)),
133 device,
134 );
135 SalpState {
136 positions,
137 fitness: Vec::new(),
138 best_genome: None,
139 best_fitness: f32::INFINITY,
140 generation: 0,
141 }
142 }
143
144 fn ask(
145 &self,
146 params: &SalpConfig,
147 state: &SalpState<B>,
148 rng: &mut dyn Rng,
149 device: &B::Device,
150 ) -> (Tensor<B, 2>, SalpState<B>) {
151 if state.fitness.is_empty() {
152 return (state.positions.clone(), state.clone());
153 }
154
155 let pop_size = params.pop_size;
156 let genome_dim = params.genome_dim;
157 let n_leaders = pop_size / 2;
158 let (lo, hi) = params.bounds;
159
160 #[allow(clippy::cast_precision_loss)]
162 let t = state.generation as f32;
163 #[allow(clippy::cast_precision_loss)]
164 let max_t = params.max_generations.max(1) as f32;
165 let frac = (4.0 * t / max_t).min(4.0);
166 let c1 = 2.0 * (-(frac * frac)).exp();
167
168 let mut stream = seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
171 let mut leader_delta: Vec<f32> = Vec::with_capacity(n_leaders * genome_dim);
172 for _ in 0..n_leaders {
173 for _ in 0..genome_dim {
174 let c2: f32 = stream.random::<f32>();
175 let c3: f32 = stream.random::<f32>();
176 let scaled = (hi - lo) * c2 + lo;
177 let sign = if c3 >= 0.5 { 1.0 } else { -1.0 };
178 leader_delta.push(sign * c1 * scaled);
179 }
180 }
181
182 let best = state
183 .best_genome
184 .as_ref()
185 .expect("best_genome populated after first tell")
186 .clone()
187 .expand([n_leaders, genome_dim]);
188 let delta = Tensor::<B, 2>::from_data(
189 TensorData::new(leader_delta, [n_leaders, genome_dim]),
190 device,
191 );
192 let new_leaders = (best + delta).clamp(lo, hi);
193
194 let followers = state
196 .positions
197 .clone()
198 .slice([n_leaders..pop_size, 0..genome_dim]);
199 let joined = Tensor::cat(vec![new_leaders.clone(), followers.clone()], 0); #[allow(clippy::cast_possible_wrap)]
206 let shift_idx: Vec<i64> = (0..(pop_size - n_leaders))
207 .map(|k| (n_leaders + k - 1) as i64)
208 .collect();
209 let idx = Tensor::<B, 1, Int>::from_data(
210 TensorData::new(shift_idx, [pop_size - n_leaders]),
211 device,
212 );
213 let previous = joined.clone().select(0, idx);
214 let new_followers = (followers + previous).mul_scalar(0.5).clamp(lo, hi);
215
216 let new_positions = Tensor::cat(vec![new_leaders, new_followers], 0);
217 let mut next = state.clone();
218 next.positions.clone_from(&new_positions);
219 (new_positions, next)
220 }
221
222 fn tell(
223 &self,
224 _params: &SalpConfig,
225 population: Tensor<B, 2>,
226 fitness: Tensor<B, 1>,
227 mut state: SalpState<B>,
228 _rng: &mut dyn Rng,
229 ) -> (SalpState<B>, StrategyMetrics) {
230 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
231 state.fitness.clone_from(&fitness_host);
232 state.positions.clone_from(&population);
233 let best_idx = argmin(&fitness_host);
234 if fitness_host[best_idx] < state.best_fitness {
235 state.best_fitness = fitness_host[best_idx];
236 let device = population.device();
237 #[allow(clippy::cast_possible_wrap)]
238 let idx = Tensor::<B, 1, Int>::from_data(
239 TensorData::new(vec![best_idx as i64], [1]),
240 &device,
241 );
242 state.best_genome = Some(population.select(0, idx));
243 }
244 state.generation += 1;
245 let m =
246 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
247 state.best_fitness = m.best_fitness_ever;
248 (state, m)
249 }
250
251 fn best(&self, state: &SalpState<B>) -> Option<(Tensor<B, 2>, f32)> {
252 state
253 .best_genome
254 .as_ref()
255 .map(|g| (g.clone(), state.best_fitness))
256 }
257}
258
259fn argmin(xs: &[f32]) -> usize {
260 let mut best_idx = 0usize;
261 let mut best = f32::INFINITY;
262 for (i, &v) in xs.iter().enumerate() {
263 if v < best {
264 best = v;
265 best_idx = i;
266 }
267 }
268 best_idx
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::fitness::FromFitnessEvaluable;
275 use crate::strategy::EvolutionaryHarness;
276 use burn::backend::NdArray;
277 use rlevo_core::fitness::FitnessEvaluable;
278
279 type TestBackend = NdArray;
280
281 struct Sphere;
282 struct SphereFit;
283 impl FitnessEvaluable for SphereFit {
284 type Individual = Vec<f64>;
285 type Landscape = Sphere;
286 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
287 x.iter().map(|v| v * v).sum()
288 }
289 }
290
291 #[test]
292 fn ssa_converges_on_sphere_d10() {
293 let device = Default::default();
298 let strategy = SalpSwarm::<TestBackend>::new();
299 let params = SalpConfig::default_for(40, 10);
300 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
301 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
302 strategy, params, fitness_fn, 3, device, 600,
303 );
304 harness.reset();
305 while !harness.step(()).done {}
306 let best = harness.latest_metrics().unwrap().best_fitness_ever;
307 assert!(best < 1e-2, "SSA D10 best={best}");
308 }
309}