1use rand::Rng;
34use rand_distr::{Beta, Distribution, Gamma};
35
36use super::model::EvolutionModel;
37use super::prior::GenomePrior;
38use crate::fitness::traits::Fitness;
39use crate::genome::trace_genome::TraceGenome;
40
41#[derive(Clone, Copy, Debug, PartialEq)]
43pub struct BetaSuccessPosterior {
44 pub alpha: f64,
46 pub beta: f64,
48}
49
50impl BetaSuccessPosterior {
51 pub fn new(alpha: f64, beta: f64) -> Self {
53 Self {
54 alpha: alpha.max(1e-6),
55 beta: beta.max(1e-6),
56 }
57 }
58
59 pub fn update(&mut self, successes: u64, failures: u64) {
61 self.alpha += successes as f64;
62 self.beta += failures as f64;
63 }
64
65 pub fn mean(&self) -> f64 {
67 self.alpha / (self.alpha + self.beta)
68 }
69
70 pub fn variance(&self) -> f64 {
72 let s = self.alpha + self.beta;
73 (self.alpha * self.beta) / (s * s * (s + 1.0))
74 }
75
76 pub fn total(&self) -> f64 {
78 self.alpha + self.beta
79 }
80
81 pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
83 Beta::new(self.alpha, self.beta)
84 .expect("valid Beta parameters")
85 .sample(rng)
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq)]
91pub struct GammaRatePosterior {
92 pub shape: f64,
94 pub rate: f64,
96}
97
98impl GammaRatePosterior {
99 pub fn new(shape: f64, rate: f64) -> Self {
101 Self {
102 shape: shape.max(1e-6),
103 rate: rate.max(1e-6),
104 }
105 }
106
107 pub fn observe(&mut self, count: u64, exposure: f64) {
110 self.shape += count as f64;
111 self.rate += exposure;
112 }
113
114 pub fn mean(&self) -> f64 {
116 self.shape / self.rate
117 }
118
119 pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
121 Gamma::new(self.shape, 1.0 / self.rate)
123 .expect("valid Gamma parameters")
124 .sample(rng)
125 }
126}
127
128#[derive(Clone, Copy, Debug)]
130pub struct OperatorArm {
131 pub sigma: f64,
133 pub posterior: BetaSuccessPosterior,
135 pub times_selected: usize,
137}
138
139impl OperatorArm {
140 pub fn new(sigma: f64) -> Self {
142 Self {
143 sigma,
144 posterior: BetaSuccessPosterior::new(1.0, 1.0),
145 times_selected: 0,
146 }
147 }
148}
149
150pub struct BayesianAdaptiveGA<P, F>
156where
157 P: GenomePrior,
158 F: Fitness<Genome = P::Genome, Value = f64> + Clone + Send + Sync + 'static,
159{
160 model: EvolutionModel<P, super::likelihood::FactorFitness<F>>,
161 population_size: usize,
162 generations: usize,
163 tournament_size: usize,
164 mutation_rate: f64,
165 arms: Vec<OperatorArm>,
166 improvement_rate: GammaRatePosterior,
167}
168
169impl<P, F> BayesianAdaptiveGA<P, F>
170where
171 P: GenomePrior,
172 F: Fitness<Genome = P::Genome, Value = f64> + Clone + Send + Sync + 'static,
173{
174 pub fn new(prior: P, fitness: F, population_size: usize, generations: usize) -> Self {
176 Self {
177 model: EvolutionModel::new(prior, fitness),
178 population_size,
179 generations,
180 tournament_size: 3,
181 mutation_rate: 0.5,
182 arms: vec![
183 OperatorArm::new(0.05),
184 OperatorArm::new(0.2),
185 OperatorArm::new(0.5),
186 OperatorArm::new(1.0),
187 ],
188 improvement_rate: GammaRatePosterior::new(1.0, 1.0),
189 }
190 }
191
192 pub fn with_step_sizes(mut self, sigmas: Vec<f64>) -> Self {
194 self.arms = sigmas.into_iter().map(OperatorArm::new).collect();
195 self
196 }
197
198 pub fn with_mutation_rate(mut self, rate: f64) -> Self {
200 self.mutation_rate = rate;
201 self
202 }
203
204 pub fn with_tournament_size(mut self, size: usize) -> Self {
206 self.tournament_size = size.max(1);
207 self
208 }
209
210 fn thompson_select<R: Rng>(&self, rng: &mut R) -> usize {
213 let mut best_idx = 0;
214 let mut best_draw = f64::NEG_INFINITY;
215 for (i, arm) in self.arms.iter().enumerate() {
216 let draw = arm.posterior.sample(rng);
217 if draw > best_draw {
218 best_draw = draw;
219 best_idx = i;
220 }
221 }
222 best_idx
223 }
224
225 pub fn run<R: Rng>(&mut self, rng: &mut R) -> BayesianAdaptiveGAResult<P::Genome> {
227 let mut population: Vec<P::Genome> = (0..self.population_size)
228 .map(|_| self.model.sample_prior(rng))
229 .collect();
230 let mut fitnesses: Vec<f64> = population
231 .iter()
232 .map(|g| self.model.fitness_value(g))
233 .collect();
234
235 let mut best_genome = population[0].clone();
236 let mut best_fitness = fitnesses[0];
237 for (g, &f) in population.iter().zip(fitnesses.iter()) {
238 if f > best_fitness {
239 best_fitness = f;
240 best_genome = g.clone();
241 }
242 }
243
244 let mut fitness_history = Vec::with_capacity(self.generations);
245 let mut selected_arm_history = Vec::with_capacity(self.generations);
246
247 for _ in 0..self.generations {
248 let arm_idx = self.thompson_select(rng);
250 self.arms[arm_idx].times_selected += 1;
251 selected_arm_history.push(arm_idx);
252 let sigma = self.arms[arm_idx].sigma;
253
254 let mut next_population = Vec::with_capacity(self.population_size);
257 let mut next_fitness = Vec::with_capacity(self.population_size);
258 let mut successes: u64 = 0;
259 let mut failures: u64 = 0;
260
261 for _ in 0..self.population_size {
262 let parent_idx = self.tournament(&fitnesses, rng);
263 let parent = &population[parent_idx];
264 let parent_fitness = fitnesses[parent_idx];
265
266 let child = gaussian_trace_mutation(parent, self.mutation_rate, sigma, rng);
267 let child_fitness = self.model.fitness_value(&child);
268
269 if child_fitness > parent_fitness {
270 successes += 1;
271 } else {
272 failures += 1;
273 }
274
275 if child_fitness > best_fitness {
276 best_fitness = child_fitness;
277 best_genome = child.clone();
278 }
279
280 next_population.push(child);
281 next_fitness.push(child_fitness);
282 }
283
284 self.arms[arm_idx].posterior.update(successes, failures);
286 self.improvement_rate.observe(successes, 1.0);
287
288 population = next_population;
289 fitnesses = next_fitness;
290
291 let mean_fitness = fitnesses.iter().sum::<f64>() / fitnesses.len() as f64;
292 fitness_history.push(mean_fitness);
293 }
294
295 BayesianAdaptiveGAResult {
296 best_genome,
297 best_fitness,
298 fitness_history,
299 selected_arm_history,
300 operator_posteriors: self.arms.clone(),
301 improvement_rate: self.improvement_rate,
302 }
303 }
304
305 fn tournament<R: Rng>(&self, fitnesses: &[f64], rng: &mut R) -> usize {
306 let mut best = rng.gen_range(0..fitnesses.len());
307 for _ in 1..self.tournament_size {
308 let challenger = rng.gen_range(0..fitnesses.len());
309 if fitnesses[challenger] > fitnesses[best] {
310 best = challenger;
311 }
312 }
313 best
314 }
315}
316
317fn gaussian_trace_mutation<G: TraceGenome, R: Rng>(
323 genome: &G,
324 rate: f64,
325 sigma: f64,
326 rng: &mut R,
327) -> G {
328 use fugue::{ChoiceValue, Trace};
329 let normal = rand_distr::Normal::new(0.0, sigma.max(1e-12)).expect("valid mutation sigma");
330 let trace = genome.to_trace();
331 let mut new_trace = Trace::default();
332 for (addr, choice) in &trace.choices {
333 let value = match &choice.value {
334 ChoiceValue::F64(v) if rng.gen::<f64>() < rate => {
335 ChoiceValue::F64(v + normal.sample(rng))
336 }
337 other => other.clone(),
338 };
339 new_trace.insert_choice(addr.clone(), value, 0.0);
340 }
341 G::from_trace(&new_trace).unwrap_or_else(|_| genome.clone())
342}
343
344pub struct BayesianAdaptiveGAResult<G> {
346 pub best_genome: G,
348 pub best_fitness: f64,
350 pub fitness_history: Vec<f64>,
352 pub selected_arm_history: Vec<usize>,
354 pub operator_posteriors: Vec<OperatorArm>,
356 pub improvement_rate: GammaRatePosterior,
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::fitness::benchmarks::Sphere;
364 use crate::genome::bounds::MultiBounds;
365 use crate::inference::prior::UniformBoxPrior;
366 use rand::rngs::StdRng;
367 use rand::SeedableRng;
368
369 #[test]
370 fn test_beta_posterior_conjugate_update() {
371 let mut post = BetaSuccessPosterior::new(2.0, 8.0);
374 assert!((post.mean() - 0.2).abs() < 1e-12);
375 post.update(5, 3);
376 assert_eq!(post.alpha, 7.0);
377 assert_eq!(post.beta, 11.0);
378 assert!((post.mean() - 7.0 / 18.0).abs() < 1e-12);
379 }
380
381 #[test]
382 fn test_beta_posterior_sampling_matches_beta_moments() {
383 let post = BetaSuccessPosterior::new(2.0, 8.0);
387 let mut rng = StdRng::seed_from_u64(2024);
388 let draws: Vec<f64> = (0..20000).map(|_| post.sample(&mut rng)).collect();
389 let mean = draws.iter().sum::<f64>() / draws.len() as f64;
390 let var = draws.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / draws.len() as f64;
391 assert!((mean - post.mean()).abs() < 0.01, "mean {}", mean);
392 assert!(
393 (var.sqrt() - post.variance().sqrt()).abs() < 0.02,
394 "std {} vs analytic {}",
395 var.sqrt(),
396 post.variance().sqrt()
397 );
398 assert!(
400 var.sqrt() > 0.06,
401 "std too small for a Beta draw: {}",
402 var.sqrt()
403 );
404 }
405
406 #[test]
407 fn test_gamma_posterior_conjugate_update() {
408 let mut post = GammaRatePosterior::new(2.0, 1.0);
409 post.observe(5, 1.0);
410 assert_eq!(post.shape, 7.0);
411 assert_eq!(post.rate, 2.0);
412 assert!((post.mean() - 3.5).abs() < 1e-12);
413 }
414
415 #[test]
416 fn test_adaptive_ga_updates_posteriors_and_improves() {
417 let fit = Sphere::new(3);
421 let bounds = MultiBounds::symmetric(5.0, 3);
422 let mut ga = BayesianAdaptiveGA::new(UniformBoxPrior::new(bounds), fit, 40, 60);
423 let mut rng = StdRng::seed_from_u64(7);
424 let result = ga.run(&mut rng);
425
426 let total_evidence: f64 = result
428 .operator_posteriors
429 .iter()
430 .map(|a| a.posterior.total() - 2.0) .sum();
432 assert!(
433 total_evidence >= (60 * 40) as f64 - 1.0,
434 "posteriors did not accumulate the expected evidence: {}",
435 total_evidence
436 );
437
438 assert!(result
440 .operator_posteriors
441 .iter()
442 .any(|a| a.times_selected > 0));
443
444 assert!(result.improvement_rate.shape > 1.0);
446
447 assert!(
449 result.best_fitness > -1.0,
450 "best fitness {} did not converge",
451 result.best_fitness
452 );
453 }
454
455 #[test]
456 fn test_thompson_prefers_better_operator() {
457 let fit = Sphere::new(2);
460 let bounds = MultiBounds::symmetric(0.5, 2);
461 let mut ga = BayesianAdaptiveGA::new(UniformBoxPrior::new(bounds), fit, 50, 80)
462 .with_step_sizes(vec![0.02, 2.0]);
463 let mut rng = StdRng::seed_from_u64(11);
464 let result = ga.run(&mut rng);
465
466 let small = result.operator_posteriors[0].posterior.mean();
467 let large = result.operator_posteriors[1].posterior.mean();
468 assert!(
469 small > large,
470 "small-step success posterior {} should exceed large-step {}",
471 small,
472 large
473 );
474 }
475}