rlevo_evolution/algorithms/
ep.rs1use std::marker::PhantomData;
22
23use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
24use rand::Rng;
25
26use crate::ops::mutation::gaussian_mutation_per_row;
27use crate::rng::{SeedPurpose, seed_stream};
28use crate::strategy::{Strategy, StrategyMetrics};
29
30#[derive(Debug, Clone)]
32pub struct EpConfig {
33 pub mu: usize,
36 pub genome_dim: usize,
38 pub bounds: (f32, f32),
40 pub initial_sigma: f32,
42 pub tau: f32,
45 pub tournament_q: usize,
47}
48
49impl EpConfig {
50 #[must_use]
52 pub fn default_for(mu: usize, genome_dim: usize) -> Self {
53 #[allow(clippy::cast_precision_loss)]
54 let d = genome_dim as f32;
55 let tau = 1.0 / (2.0 * d.sqrt()).sqrt();
56 Self {
57 mu,
58 genome_dim,
59 bounds: (-5.12, 5.12),
60 initial_sigma: 1.0,
61 tau,
62 tournament_q: 10,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct EpState<B: Backend> {
70 pub parents: Tensor<B, 2>,
72 pub sigmas: Tensor<B, 1>,
74 pub parent_fitness: Vec<f32>,
76 pub best_genome: Option<Tensor<B, 2>>,
78 pub best_fitness: f32,
80 pub generation: usize,
82}
83
84#[derive(Debug, Clone, Copy, Default)]
97pub struct EvolutionaryProgramming<B: Backend> {
98 _backend: PhantomData<fn() -> B>,
99}
100
101impl<B: Backend> EvolutionaryProgramming<B> {
102 #[must_use]
104 pub fn new() -> Self {
105 Self {
106 _backend: PhantomData,
107 }
108 }
109}
110
111impl<B: Backend> Strategy<B> for EvolutionaryProgramming<B>
112where
113 B::Device: Clone,
114{
115 type Params = EpConfig;
116 type State = EpState<B>;
117 type Genome = Tensor<B, 2>;
118
119 fn init(&self, params: &EpConfig, rng: &mut dyn Rng, device: &B::Device) -> EpState<B> {
120 let (lo, hi) = params.bounds;
121 B::seed(device, rng.next_u64());
122 let parents = Tensor::<B, 2>::random(
123 [params.mu, params.genome_dim],
124 burn::tensor::Distribution::Uniform(f64::from(lo), f64::from(hi)),
125 device,
126 );
127 let sigmas = Tensor::<B, 1>::from_data(
128 TensorData::new(vec![params.initial_sigma; params.mu], [params.mu]),
129 device,
130 );
131 EpState {
132 parents,
133 sigmas,
134 parent_fitness: Vec::new(),
135 best_genome: None,
136 best_fitness: f32::INFINITY,
137 generation: 0,
138 }
139 }
140
141 fn ask(
142 &self,
143 params: &EpConfig,
144 state: &EpState<B>,
145 rng: &mut dyn Rng,
146 device: &B::Device,
147 ) -> (Tensor<B, 2>, EpState<B>) {
148 if state.parent_fitness.is_empty() {
150 return (state.parents.clone(), state.clone());
151 }
152
153 let mu = params.mu;
154 let mut sigma_rng =
155 seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
156 let mut mutation_rng = seed_stream(
157 rng.next_u64(),
158 state.generation as u64,
159 SeedPurpose::Mutation,
160 );
161
162 B::seed(device, sigma_rng.next_u64());
164 let noise =
165 Tensor::<B, 1>::random([mu], burn::tensor::Distribution::Normal(0.0, 1.0), device);
166 let offspring_sigmas = state.sigmas.clone() * noise.mul_scalar(params.tau).exp();
167
168 B::seed(device, mutation_rng.next_u64());
170 let offspring =
171 gaussian_mutation_per_row(state.parents.clone(), offspring_sigmas.clone(), device);
172 let (lo, hi) = params.bounds;
173 let offspring = offspring.clamp(lo, hi);
174
175 let mut state = state.clone();
177 state.sigmas = Tensor::cat(vec![state.sigmas.clone(), offspring_sigmas], 0);
178 (offspring, state)
179 }
180
181 fn tell(
182 &self,
183 params: &EpConfig,
184 offspring: Tensor<B, 2>,
185 fitness: Tensor<B, 1>,
186 mut state: EpState<B>,
187 rng: &mut dyn Rng,
188 ) -> (EpState<B>, StrategyMetrics) {
189 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
190 let device = offspring.device();
191
192 if state.parent_fitness.is_empty() {
194 state.parent_fitness.clone_from(&fitness_host);
195 state.generation += 1;
196 update_best(&mut state, &offspring, &fitness_host);
197 let m = StrategyMetrics::from_host_fitness(
198 state.generation,
199 &fitness_host,
200 state.best_fitness,
201 );
202 state.best_fitness = m.best_fitness_ever;
203 state.parents = offspring;
204 state.sigmas = Tensor::<B, 1>::from_data(
205 TensorData::new(vec![params.initial_sigma; params.mu], [params.mu]),
206 &device,
207 );
208 return (state, m);
209 }
210
211 let mu = params.mu;
212 let combined_pop = Tensor::cat(vec![state.parents.clone(), offspring.clone()], 0);
214 let combined_fit: Vec<f32> = state
215 .parent_fitness
216 .iter()
217 .chain(fitness_host.iter())
218 .copied()
219 .collect();
220 let combined_sigmas = state.sigmas.clone(); let mut selection_rng = seed_stream(
226 rng.next_u64(),
227 state.generation as u64,
228 SeedPurpose::Selection,
229 );
230 let n = combined_fit.len();
231 let mut win_counts: Vec<u32> = vec![0; n];
232 for (i, &my_fit) in combined_fit.iter().enumerate() {
233 for _ in 0..params.tournament_q {
234 use rand::RngExt;
235 let opp = selection_rng.random_range(0..n);
236 if my_fit < combined_fit[opp] {
237 win_counts[i] += 1;
238 }
239 }
240 }
241
242 let mut indexed: Vec<usize> = (0..n).collect();
244 indexed.sort_by(|&a, &b| {
245 win_counts[b].cmp(&win_counts[a]).then_with(|| {
246 combined_fit[a]
247 .partial_cmp(&combined_fit[b])
248 .unwrap_or(std::cmp::Ordering::Equal)
249 })
250 });
251 indexed.truncate(mu);
252 #[allow(clippy::cast_possible_wrap)]
253 let survivor_idx: Vec<i64> = indexed.iter().map(|&i| i as i64).collect();
254
255 let idx_tensor =
256 Tensor::<B, 1, Int>::from_data(TensorData::new(survivor_idx.clone(), [mu]), &device);
257 let next_parents = combined_pop.select(0, idx_tensor.clone());
258 let next_sigmas = combined_sigmas.select(0, idx_tensor);
259 let next_fitness: Vec<f32> = survivor_idx
260 .iter()
261 .map(|&i| {
262 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
263 combined_fit[i as usize]
264 })
265 .collect();
266
267 state.parents = next_parents;
268 state.sigmas = next_sigmas;
269 state.parent_fitness = next_fitness;
270 state.generation += 1;
271 update_best(&mut state, &offspring, &fitness_host);
272 let m =
273 StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
274 state.best_fitness = m.best_fitness_ever;
275 (state, m)
276 }
277
278 fn best(&self, state: &EpState<B>) -> Option<(Tensor<B, 2>, f32)> {
279 state
280 .best_genome
281 .as_ref()
282 .map(|g| (g.clone(), state.best_fitness))
283 }
284}
285
286fn update_best<B: Backend>(state: &mut EpState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
287 if fitness.is_empty() {
288 return;
289 }
290 let mut best_idx = 0usize;
291 let mut best_f = fitness[0];
292 for (i, &f) in fitness.iter().enumerate().skip(1) {
293 if f < best_f {
294 best_f = f;
295 best_idx = i;
296 }
297 }
298 if best_f < state.best_fitness {
299 let device = pop.device();
300 #[allow(clippy::cast_possible_wrap)]
301 let idx =
302 Tensor::<B, 1, Int>::from_data(TensorData::new(vec![best_idx as i64], [1]), &device);
303 state.best_genome = Some(pop.clone().select(0, idx));
304 state.best_fitness = best_f;
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::fitness::FromFitnessEvaluable;
312 use crate::strategy::EvolutionaryHarness;
313 use burn::backend::NdArray;
314 use rlevo_core::fitness::FitnessEvaluable;
315 type TestBackend = NdArray;
316
317 struct Sphere;
318 struct SphereFit;
319 impl FitnessEvaluable for SphereFit {
320 type Individual = Vec<f64>;
321 type Landscape = Sphere;
322 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
323 x.iter().map(|v| v * v).sum()
324 }
325 }
326
327 #[test]
328 fn ep_converges_on_sphere_d2() {
329 let device = Default::default();
330 let params = EpConfig::default_for(10, 2);
331 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
332 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
333 EvolutionaryProgramming::<TestBackend>::new(),
334 params,
335 fitness_fn,
336 3,
337 device,
338 300,
339 );
340 harness.reset();
341 loop {
342 if harness.step(()).done {
343 break;
344 }
345 }
346 let best = harness.latest_metrics().unwrap().best_fitness_ever;
347 assert!(best < 1e-2, "EP Sphere-D2 best={best}");
348 }
349
350 #[test]
351 fn ep_converges_on_sphere_d10() {
352 let device = Default::default();
353 let params = EpConfig::default_for(20, 10);
354 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
355 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
356 EvolutionaryProgramming::<TestBackend>::new(),
357 params,
358 fitness_fn,
359 5,
360 device,
361 2000,
362 );
363 harness.reset();
364 loop {
365 if harness.step(()).done {
366 break;
367 }
368 }
369 let best = harness.latest_metrics().unwrap().best_fitness_ever;
370 assert!(best < 1e-4, "EP Sphere-D10 best={best}");
371 }
372}