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>
169where
170 P: GenomePrior,
171 F: Fitness<Genome = P::Genome, Value = f64> + Clone + Send + Sync + 'static,
172{
173 model: EvolutionModel<P, super::likelihood::FactorFitness<F>>,
174 population_size: usize,
175 generations: usize,
176 tournament_size: usize,
177 mutation_rate: f64,
178 arms: Vec<OperatorArm>,
179 improvement_rate: GammaRatePosterior,
180}
181
182impl<P, F> BayesianAdaptiveGA<P, F>
183where
184 P: GenomePrior,
185 F: Fitness<Genome = P::Genome, Value = f64> + Clone + Send + Sync + 'static,
186{
187 pub fn new(prior: P, fitness: F, population_size: usize, generations: usize) -> Self {
189 Self {
190 model: EvolutionModel::new(prior, fitness),
191 population_size,
192 generations,
193 tournament_size: 3,
194 mutation_rate: 0.5,
195 arms: vec![
196 OperatorArm::new(0.05),
197 OperatorArm::new(0.2),
198 OperatorArm::new(0.5),
199 OperatorArm::new(1.0),
200 ],
201 improvement_rate: GammaRatePosterior::new(1.0, 1.0),
202 }
203 }
204
205 pub fn with_step_sizes(mut self, sigmas: Vec<f64>) -> Self {
207 self.arms = sigmas.into_iter().map(OperatorArm::new).collect();
208 self
209 }
210
211 pub fn with_mutation_rate(mut self, rate: f64) -> Self {
213 self.mutation_rate = rate;
214 self
215 }
216
217 pub fn with_tournament_size(mut self, size: usize) -> Self {
219 self.tournament_size = size.max(1);
220 self
221 }
222
223 fn thompson_select<R: Rng>(&self, rng: &mut R) -> usize {
226 let mut best_idx = 0;
227 let mut best_draw = f64::NEG_INFINITY;
228 for (i, arm) in self.arms.iter().enumerate() {
229 let draw = arm.posterior.sample(rng);
230 if draw > best_draw {
231 best_draw = draw;
232 best_idx = i;
233 }
234 }
235 best_idx
236 }
237
238 pub fn run<R: Rng>(&mut self, rng: &mut R) -> BayesianAdaptiveGAResult<P::Genome> {
240 let mut population: Vec<P::Genome> = (0..self.population_size)
241 .map(|_| self.model.sample_prior(rng))
242 .collect();
243 let mut fitnesses: Vec<f64> = population
244 .iter()
245 .map(|g| self.model.fitness_value(g))
246 .collect();
247
248 let mut best_genome = population[0].clone();
249 let mut best_fitness = fitnesses[0];
250 for (g, &f) in population.iter().zip(fitnesses.iter()) {
251 if f > best_fitness {
252 best_fitness = f;
253 best_genome = g.clone();
254 }
255 }
256
257 let mut fitness_history = Vec::with_capacity(self.generations);
258 let mut selected_arm_history = Vec::with_capacity(self.generations);
259
260 for _ in 0..self.generations {
261 let arm_idx = self.thompson_select(rng);
263 self.arms[arm_idx].times_selected += 1;
264 selected_arm_history.push(arm_idx);
265 let sigma = self.arms[arm_idx].sigma;
266
267 let mut next_population = Vec::with_capacity(self.population_size);
270 let mut next_fitness = Vec::with_capacity(self.population_size);
271 let mut successes: u64 = 0;
272 let mut failures: u64 = 0;
273
274 for _ in 0..self.population_size {
275 let parent_idx = self.tournament(&fitnesses, rng);
276 let parent = &population[parent_idx];
277 let parent_fitness = fitnesses[parent_idx];
278
279 let mutant = gaussian_trace_mutation(parent, self.mutation_rate, sigma, rng);
280 let (child, child_fitness) = if self.in_prior_support(&mutant) {
285 let f = self.model.fitness_value(&mutant);
286 (mutant, f)
287 } else {
288 (parent.clone(), parent_fitness)
289 };
290
291 if child_fitness > parent_fitness {
292 successes += 1;
293 } else {
294 failures += 1;
295 }
296
297 if child_fitness > best_fitness {
298 best_fitness = child_fitness;
299 best_genome = child.clone();
300 }
301
302 next_population.push(child);
303 next_fitness.push(child_fitness);
304 }
305
306 self.arms[arm_idx].posterior.update(successes, failures);
308 self.improvement_rate.observe(successes, 1.0);
309
310 population = next_population;
311 fitnesses = next_fitness;
312
313 let mean_fitness = fitnesses.iter().sum::<f64>() / fitnesses.len() as f64;
314 fitness_history.push(mean_fitness);
315 }
316
317 BayesianAdaptiveGAResult {
318 best_genome,
319 best_fitness,
320 fitness_history,
321 selected_arm_history,
322 operator_posteriors: self.arms.clone(),
323 improvement_rate: self.improvement_rate,
324 }
325 }
326
327 fn in_prior_support(&self, genome: &P::Genome) -> bool {
331 let prior = self.model.prior();
332 prior.validate(genome).is_ok()
333 && super::model::score_complete(prior.trace_of(genome), prior.model())
334 .map(|(_g, t)| t.log_prior.is_finite())
335 .unwrap_or(false)
336 }
337
338 fn tournament<R: Rng>(&self, fitnesses: &[f64], rng: &mut R) -> usize {
339 let mut best = rng.gen_range(0..fitnesses.len());
340 for _ in 1..self.tournament_size {
341 let challenger = rng.gen_range(0..fitnesses.len());
342 if fitnesses[challenger] > fitnesses[best] {
343 best = challenger;
344 }
345 }
346 best
347 }
348}
349
350fn gaussian_trace_mutation<G: TraceGenome, R: Rng>(
356 genome: &G,
357 rate: f64,
358 sigma: f64,
359 rng: &mut R,
360) -> G {
361 use fugue::{ChoiceValue, Trace};
362 let normal = rand_distr::Normal::new(0.0, sigma.max(1e-12)).expect("valid mutation sigma");
363 let trace = genome.to_trace();
364 let mut new_trace = Trace::default();
365 for (addr, choice) in &trace.choices {
366 let value = match &choice.value {
367 ChoiceValue::F64(v) if rng.gen::<f64>() < rate => {
368 ChoiceValue::F64(v + normal.sample(rng))
369 }
370 other => other.clone(),
371 };
372 new_trace.insert_choice(addr.clone(), value, 0.0);
373 }
374 G::from_trace(&new_trace).unwrap_or_else(|_| genome.clone())
375}
376
377pub struct BayesianAdaptiveGAResult<G> {
379 pub best_genome: G,
381 pub best_fitness: f64,
383 pub fitness_history: Vec<f64>,
385 pub selected_arm_history: Vec<usize>,
387 pub operator_posteriors: Vec<OperatorArm>,
389 pub improvement_rate: GammaRatePosterior,
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::fitness::benchmarks::Sphere;
397 use crate::genome::bounds::MultiBounds;
398 use crate::inference::prior::UniformBoxPrior;
399 use rand::rngs::StdRng;
400 use rand::SeedableRng;
401
402 #[test]
403 fn test_beta_posterior_conjugate_update() {
404 let mut post = BetaSuccessPosterior::new(2.0, 8.0);
407 assert!((post.mean() - 0.2).abs() < 1e-12);
408 post.update(5, 3);
409 assert_eq!(post.alpha, 7.0);
410 assert_eq!(post.beta, 11.0);
411 assert!((post.mean() - 7.0 / 18.0).abs() < 1e-12);
412 }
413
414 #[test]
415 fn test_beta_posterior_sampling_matches_beta_moments() {
416 let post = BetaSuccessPosterior::new(2.0, 8.0);
420 let mut rng = StdRng::seed_from_u64(2024);
421 let draws: Vec<f64> = (0..20000).map(|_| post.sample(&mut rng)).collect();
422 let mean = draws.iter().sum::<f64>() / draws.len() as f64;
423 let var = draws.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / draws.len() as f64;
424 assert!((mean - post.mean()).abs() < 0.01, "mean {}", mean);
425 assert!(
426 (var.sqrt() - post.variance().sqrt()).abs() < 0.02,
427 "std {} vs analytic {}",
428 var.sqrt(),
429 post.variance().sqrt()
430 );
431 assert!(
433 var.sqrt() > 0.06,
434 "std too small for a Beta draw: {}",
435 var.sqrt()
436 );
437 }
438
439 #[test]
440 fn test_gamma_posterior_conjugate_update() {
441 let mut post = GammaRatePosterior::new(2.0, 1.0);
442 post.observe(5, 1.0);
443 assert_eq!(post.shape, 7.0);
444 assert_eq!(post.rate, 2.0);
445 assert!((post.mean() - 3.5).abs() < 1e-12);
446 }
447
448 #[test]
449 fn test_adaptive_ga_updates_posteriors_and_improves() {
450 let fit = Sphere::new(3);
454 let bounds = MultiBounds::symmetric(5.0, 3);
455 let mut ga = BayesianAdaptiveGA::new(UniformBoxPrior::new(bounds), fit, 40, 60);
456 let mut rng = StdRng::seed_from_u64(7);
457 let result = ga.run(&mut rng);
458
459 let total_evidence: f64 = result
461 .operator_posteriors
462 .iter()
463 .map(|a| a.posterior.total() - 2.0) .sum();
465 assert!(
466 total_evidence >= (60 * 40) as f64 - 1.0,
467 "posteriors did not accumulate the expected evidence: {}",
468 total_evidence
469 );
470
471 assert!(result
473 .operator_posteriors
474 .iter()
475 .any(|a| a.times_selected > 0));
476
477 assert!(result.improvement_rate.shape > 1.0);
479
480 assert!(
482 result.best_fitness > -1.0,
483 "best fitness {} did not converge",
484 result.best_fitness
485 );
486 }
487
488 #[test]
493 fn test_children_stay_inside_prior_support() {
494 use crate::genome::real_vector::RealVector;
495 use crate::genome::traits::RealValuedGenome;
496 #[derive(Clone, Copy)]
497 struct Outward;
498 impl Fitness for Outward {
499 type Genome = RealVector;
500 type Value = f64;
501 fn evaluate(&self, g: &RealVector) -> f64 {
502 g.genes().iter().sum()
503 }
504 }
505 let bounds = MultiBounds::symmetric(0.5, 2);
506 let mut ga = BayesianAdaptiveGA::new(UniformBoxPrior::new(bounds), Outward, 30, 40)
507 .with_step_sizes(vec![0.3]);
508 let mut rng = StdRng::seed_from_u64(3);
509 let result = ga.run(&mut rng);
510 for x in result.best_genome.genes() {
511 assert!(
512 (-0.5..=0.5).contains(x),
513 "best genome left the prior's box: {x}"
514 );
515 }
516 assert!(result.best_fitness <= 1.0 + 1e-12);
518 assert!(
519 result.best_fitness > 0.8,
520 "did not approach the box corner: {}",
521 result.best_fitness
522 );
523 }
524
525 #[test]
526 fn test_thompson_prefers_better_operator() {
527 let fit = Sphere::new(2);
530 let bounds = MultiBounds::symmetric(0.5, 2);
531 let mut ga = BayesianAdaptiveGA::new(UniformBoxPrior::new(bounds), fit, 50, 80)
532 .with_step_sizes(vec![0.02, 2.0]);
533 let mut rng = StdRng::seed_from_u64(11);
534 let result = ga.run(&mut rng);
535
536 let small = result.operator_posteriors[0].posterior.mean();
537 let large = result.operator_posteriors[1].posterior.mean();
538 assert!(
539 small > large,
540 "small-step success posterior {} should exceed large-step {}",
541 small,
542 large
543 );
544 }
545}