use fugue::{Beta, Distribution, Gamma};
use rand::Rng;
use super::evolution_model::{EvolutionChainConfig, EvolutionModel, EvolutionStep};
use crate::fitness::traits::Fitness;
use crate::genome::bounds::MultiBounds;
use crate::genome::traits::EvolutionaryGenome;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BetaSuccessPosterior {
pub alpha: f64,
pub beta: f64,
}
impl BetaSuccessPosterior {
pub fn new(alpha: f64, beta: f64) -> Self {
Self {
alpha: alpha.max(1e-6),
beta: beta.max(1e-6),
}
}
pub fn update(&mut self, successes: u64, failures: u64) {
self.alpha += successes as f64;
self.beta += failures as f64;
}
pub fn mean(&self) -> f64 {
self.alpha / (self.alpha + self.beta)
}
pub fn variance(&self) -> f64 {
let s = self.alpha + self.beta;
(self.alpha * self.beta) / (s * s * (s + 1.0))
}
pub fn total(&self) -> f64 {
self.alpha + self.beta
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
Beta::new(self.alpha, self.beta)
.expect("valid Beta parameters")
.sample(rng)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GammaRatePosterior {
pub shape: f64,
pub rate: f64,
}
impl GammaRatePosterior {
pub fn new(shape: f64, rate: f64) -> Self {
Self {
shape: shape.max(1e-6),
rate: rate.max(1e-6),
}
}
pub fn observe(&mut self, count: u64, exposure: f64) {
self.shape += count as f64;
self.rate += exposure;
}
pub fn mean(&self) -> f64 {
self.shape / self.rate
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
Gamma::new(self.shape, self.rate)
.expect("valid Gamma parameters")
.sample(rng)
}
}
#[derive(Clone, Copy, Debug)]
pub struct OperatorArm {
pub sigma: f64,
pub posterior: BetaSuccessPosterior,
pub times_selected: usize,
}
impl OperatorArm {
pub fn new(sigma: f64) -> Self {
Self {
sigma,
posterior: BetaSuccessPosterior::new(1.0, 1.0),
times_selected: 0,
}
}
}
pub struct BayesianAdaptiveGA<G, F>
where
G: EvolutionaryGenome,
F: Fitness<Genome = G, Value = f64>,
{
model: EvolutionModel<G, F>,
population_size: usize,
generations: usize,
tournament_size: usize,
mutation_rate: f64,
arms: Vec<OperatorArm>,
improvement_rate: GammaRatePosterior,
}
impl<G, F> BayesianAdaptiveGA<G, F>
where
G: EvolutionaryGenome + Clone,
F: Fitness<Genome = G, Value = f64> + Clone,
{
pub fn new(
fitness: F,
bounds: MultiBounds,
population_size: usize,
generations: usize,
) -> Self {
Self {
model: EvolutionModel::new(fitness, bounds),
population_size,
generations,
tournament_size: 3,
mutation_rate: 0.5,
arms: vec![
OperatorArm::new(0.05),
OperatorArm::new(0.2),
OperatorArm::new(0.5),
OperatorArm::new(1.0),
],
improvement_rate: GammaRatePosterior::new(1.0, 1.0),
}
}
pub fn with_step_sizes(mut self, sigmas: Vec<f64>) -> Self {
self.arms = sigmas.into_iter().map(OperatorArm::new).collect();
self
}
pub fn with_mutation_rate(mut self, rate: f64) -> Self {
self.mutation_rate = rate;
self
}
pub fn with_tournament_size(mut self, size: usize) -> Self {
self.tournament_size = size.max(1);
self
}
fn thompson_select<R: Rng>(&self, rng: &mut R) -> usize {
let mut best_idx = 0;
let mut best_draw = f64::NEG_INFINITY;
for (i, arm) in self.arms.iter().enumerate() {
let draw = arm.posterior.sample(rng);
if draw > best_draw {
best_draw = draw;
best_idx = i;
}
}
best_idx
}
pub fn run<R: Rng>(&mut self, rng: &mut R) -> BayesianAdaptiveGAResult<G> {
let mut population: Vec<G> = (0..self.population_size)
.map(|_| self.model.sample_prior(rng))
.collect();
let mut fitnesses: Vec<f64> = population
.iter()
.map(|g| self.model.fitness_value(g))
.collect();
let mut best_genome = population[0].clone();
let mut best_fitness = fitnesses[0];
for (g, &f) in population.iter().zip(fitnesses.iter()) {
if f > best_fitness {
best_fitness = f;
best_genome = g.clone();
}
}
let mut fitness_history = Vec::with_capacity(self.generations);
let mut selected_arm_history = Vec::with_capacity(self.generations);
for _ in 0..self.generations {
let arm_idx = self.thompson_select(rng);
self.arms[arm_idx].times_selected += 1;
selected_arm_history.push(arm_idx);
let sigma = self.arms[arm_idx].sigma;
let step = EvolutionStep::new(
self.model.clone(),
EvolutionChainConfig::default()
.mutation_rate(self.mutation_rate)
.mutation_sigma(sigma),
);
let mut next_population = Vec::with_capacity(self.population_size);
let mut next_fitness = Vec::with_capacity(self.population_size);
let mut successes: u64 = 0;
let mut failures: u64 = 0;
for _ in 0..self.population_size {
let parent_idx = self.tournament(&fitnesses, rng);
let parent = &population[parent_idx];
let parent_fitness = fitnesses[parent_idx];
let child = step.propose(parent, rng);
let child_fitness = self.model.fitness_value(&child);
if child_fitness > parent_fitness {
successes += 1;
} else {
failures += 1;
}
if child_fitness > best_fitness {
best_fitness = child_fitness;
best_genome = child.clone();
}
next_population.push(child);
next_fitness.push(child_fitness);
}
self.arms[arm_idx].posterior.update(successes, failures);
self.improvement_rate.observe(successes, 1.0);
population = next_population;
fitnesses = next_fitness;
let mean_fitness = fitnesses.iter().sum::<f64>() / fitnesses.len() as f64;
fitness_history.push(mean_fitness);
}
BayesianAdaptiveGAResult {
best_genome,
best_fitness,
fitness_history,
selected_arm_history,
operator_posteriors: self.arms.clone(),
improvement_rate: self.improvement_rate,
}
}
fn tournament<R: Rng>(&self, fitnesses: &[f64], rng: &mut R) -> usize {
let mut best = rng.gen_range(0..fitnesses.len());
for _ in 1..self.tournament_size {
let challenger = rng.gen_range(0..fitnesses.len());
if fitnesses[challenger] > fitnesses[best] {
best = challenger;
}
}
best
}
}
pub struct BayesianAdaptiveGAResult<G> {
pub best_genome: G,
pub best_fitness: f64,
pub fitness_history: Vec<f64>,
pub selected_arm_history: Vec<usize>,
pub operator_posteriors: Vec<OperatorArm>,
pub improvement_rate: GammaRatePosterior,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fitness::benchmarks::Sphere;
use crate::genome::bounds::MultiBounds;
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn test_beta_posterior_conjugate_update() {
let mut post = BetaSuccessPosterior::new(2.0, 8.0);
assert!((post.mean() - 0.2).abs() < 1e-12);
post.update(5, 3);
assert_eq!(post.alpha, 7.0);
assert_eq!(post.beta, 11.0);
assert!((post.mean() - 7.0 / 18.0).abs() < 1e-12);
}
#[test]
fn test_beta_posterior_sampling_matches_beta_moments() {
let post = BetaSuccessPosterior::new(2.0, 8.0);
let mut rng = StdRng::seed_from_u64(2024);
let draws: Vec<f64> = (0..20000).map(|_| post.sample(&mut rng)).collect();
let mean = draws.iter().sum::<f64>() / draws.len() as f64;
let var = draws.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / draws.len() as f64;
assert!((mean - post.mean()).abs() < 0.01, "mean {}", mean);
assert!(
(var.sqrt() - post.variance().sqrt()).abs() < 0.02,
"std {} vs analytic {}",
var.sqrt(),
post.variance().sqrt()
);
assert!(
var.sqrt() > 0.06,
"std too small for a Beta draw: {}",
var.sqrt()
);
}
#[test]
fn test_gamma_posterior_conjugate_update() {
let mut post = GammaRatePosterior::new(2.0, 1.0);
post.observe(5, 1.0);
assert_eq!(post.shape, 7.0);
assert_eq!(post.rate, 2.0);
assert!((post.mean() - 3.5).abs() < 1e-12);
}
#[test]
fn test_adaptive_ga_updates_posteriors_and_improves() {
let fit = Sphere::new(3);
let bounds = MultiBounds::symmetric(5.0, 3);
let mut ga = BayesianAdaptiveGA::new(fit, bounds, 40, 60);
let mut rng = StdRng::seed_from_u64(7);
let result = ga.run(&mut rng);
let total_evidence: f64 = result
.operator_posteriors
.iter()
.map(|a| a.posterior.total() - 2.0) .sum();
assert!(
total_evidence >= (60 * 40) as f64 - 1.0,
"posteriors did not accumulate the expected evidence: {}",
total_evidence
);
assert!(result
.operator_posteriors
.iter()
.any(|a| a.times_selected > 0));
assert!(result.improvement_rate.shape > 1.0);
assert!(
result.best_fitness > -1.0,
"best fitness {} did not converge",
result.best_fitness
);
}
#[test]
fn test_thompson_prefers_better_operator() {
let fit = Sphere::new(2);
let bounds = MultiBounds::symmetric(0.5, 2);
let mut ga = BayesianAdaptiveGA::new(fit, bounds, 50, 80).with_step_sizes(vec![0.02, 2.0]);
let mut rng = StdRng::seed_from_u64(11);
let result = ga.run(&mut rng);
let small = result.operator_posteriors[0].posterior.mean();
let large = result.operator_posteriors[1].posterior.mean();
assert!(
small > large,
"small-step success posterior {} should exceed large-step {}",
small,
large
);
}
}