use std::time::Instant;
use rand::Rng;
use rand_distr::{Bernoulli, Distribution, Normal};
use crate::diagnostics::{EvolutionResult, EvolutionStats, GenerationStats, TimingStats};
use crate::error::EvolutionError;
use crate::fitness::traits::{Fitness, FitnessValue};
use crate::genome::bit_string::BitString;
use crate::genome::bounds::MultiBounds;
use crate::genome::real_vector::RealVector;
use crate::genome::traits::{BinaryGenome, EvolutionaryGenome, RealValuedGenome};
use crate::population::individual::Individual;
use crate::population::population::Population;
use crate::termination::{EvolutionState, MaxGenerations, TerminationCriterion};
fn select_top_genomes<G, F>(population: &Population<G, F>, count: usize) -> Vec<&G>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
let mut ranked: Vec<&Individual<G, F>> = population.iter().collect();
ranked.sort_by(|a, b| {
if a.is_better_than(b) {
std::cmp::Ordering::Less
} else if b.is_better_than(a) {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
});
ranked
.into_iter()
.take(count)
.map(|ind| ind.genome())
.collect()
}
#[derive(Clone, Debug)]
pub struct UMDAConfig {
pub population_size: usize,
pub selection_ratio: f64,
pub min_variance: f64,
pub prob_bounds: (f64, f64),
pub learning_rate: f64,
}
impl Default for UMDAConfig {
fn default() -> Self {
Self {
population_size: 100,
selection_ratio: 0.5,
min_variance: 0.01,
prob_bounds: (0.01, 0.99),
learning_rate: 1.0,
}
}
}
impl UMDAConfig {
pub fn validate(&self) -> Result<(), EvolutionError> {
if self.population_size == 0 {
return Err(EvolutionError::Configuration(
"population_size must be >= 1".to_string(),
));
}
if !(self.selection_ratio > 0.0 && self.selection_ratio <= 1.0) {
return Err(EvolutionError::Configuration(format!(
"selection_ratio must be in (0.0, 1.0], got {}",
self.selection_ratio
)));
}
if !(self.learning_rate > 0.0 && self.learning_rate <= 1.0) {
return Err(EvolutionError::Configuration(format!(
"learning_rate must be in (0.0, 1.0], got {}",
self.learning_rate
)));
}
if self.min_variance < 0.0 {
return Err(EvolutionError::Configuration(format!(
"min_variance must be >= 0.0, got {}",
self.min_variance
)));
}
let (pmin, pmax) = self.prob_bounds;
if !(pmin > 0.0 && pmin <= pmax && pmax < 1.0) {
return Err(EvolutionError::Configuration(format!(
"prob_bounds must satisfy 0.0 < min <= max < 1.0, got ({pmin}, {pmax})"
)));
}
Ok(())
}
}
pub struct UMDABuilder<G, F, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
config: UMDAConfig,
bounds: Option<MultiBounds>,
fitness: Option<Fit>,
termination: Option<Term>,
_phantom: std::marker::PhantomData<(G, F)>,
}
impl<G, F> UMDABuilder<G, F, (), ()>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub fn new() -> Self {
Self {
config: UMDAConfig::default(),
bounds: None,
fitness: None,
termination: None,
_phantom: std::marker::PhantomData,
}
}
}
impl<G, F> Default for UMDABuilder<G, F, (), ()>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
fn default() -> Self {
Self::new()
}
}
impl<G, F, Fit, Term> UMDABuilder<G, F, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub fn population_size(mut self, size: usize) -> Self {
self.config.population_size = size;
self
}
pub fn selection_ratio(mut self, ratio: f64) -> Self {
self.config.selection_ratio = ratio;
self
}
pub fn min_variance(mut self, variance: f64) -> Self {
self.config.min_variance = variance;
self
}
pub fn prob_bounds(mut self, min: f64, max: f64) -> Self {
self.config.prob_bounds = (min, max);
self
}
pub fn learning_rate(mut self, rate: f64) -> Self {
self.config.learning_rate = rate;
self
}
pub fn bounds(mut self, bounds: MultiBounds) -> Self {
self.bounds = Some(bounds);
self
}
pub fn fitness<NewFit>(self, fitness: NewFit) -> UMDABuilder<G, F, NewFit, Term>
where
NewFit: Fitness<Genome = G, Value = F>,
{
UMDABuilder {
config: self.config,
bounds: self.bounds,
fitness: Some(fitness),
termination: self.termination,
_phantom: std::marker::PhantomData,
}
}
pub fn termination<NewTerm>(self, termination: NewTerm) -> UMDABuilder<G, F, Fit, NewTerm>
where
NewTerm: TerminationCriterion<G, F>,
{
UMDABuilder {
config: self.config,
bounds: self.bounds,
fitness: self.fitness,
termination: Some(termination),
_phantom: std::marker::PhantomData,
}
}
pub fn max_generations(self, max: usize) -> UMDABuilder<G, F, Fit, MaxGenerations> {
UMDABuilder {
config: self.config,
bounds: self.bounds,
fitness: self.fitness,
termination: Some(MaxGenerations::new(max)),
_phantom: std::marker::PhantomData,
}
}
}
#[derive(Clone, Debug)]
pub struct ContinuousUnivariateModel {
pub means: Vec<f64>,
pub variances: Vec<f64>,
}
impl ContinuousUnivariateModel {
pub const MAX_REJECTION_RETRIES: usize = 100;
pub fn from_bounds(bounds: &MultiBounds) -> Self {
let means: Vec<f64> = bounds.bounds.iter().map(|b| b.center()).collect();
let variances: Vec<f64> = bounds
.bounds
.iter()
.map(|b| (b.range() / 4.0).powi(2))
.collect();
Self { means, variances }
}
pub fn update(&mut self, selected: &[&RealVector], config: &UMDAConfig) {
let n = selected.len() as f64;
if n == 0.0 {
return;
}
for i in 0..self.means.len() {
let mean: f64 = selected.iter().map(|g| g.genes()[i]).sum::<f64>() / n;
let sum_sq: f64 = selected
.iter()
.map(|g| (g.genes()[i] - mean).powi(2))
.sum::<f64>();
let variance: f64 = if n > 1.0 { sum_sq / (n - 1.0) } else { 0.0 };
self.means[i] =
config.learning_rate * mean + (1.0 - config.learning_rate) * self.means[i];
self.variances[i] = config.learning_rate * variance.max(config.min_variance)
+ (1.0 - config.learning_rate) * self.variances[i];
}
}
pub fn sample<R: Rng>(&self, bounds: &MultiBounds, rng: &mut R) -> RealVector {
let genes: Vec<f64> = self
.means
.iter()
.zip(self.variances.iter())
.zip(bounds.bounds.iter())
.map(|((mean, var), bound)| {
let normal =
Normal::new(*mean, var.sqrt()).unwrap_or(Normal::new(*mean, 0.1).unwrap());
let mut value = normal.sample(rng);
let mut retries = 0;
while (value < bound.min || value > bound.max)
&& retries < Self::MAX_REJECTION_RETRIES
{
value = normal.sample(rng);
retries += 1;
}
value.clamp(bound.min, bound.max)
})
.collect();
RealVector::new(genes)
}
}
impl<F, Fit, Term> UMDABuilder<RealVector, F, Fit, Term>
where
F: FitnessValue + Send,
Fit: Fitness<Genome = RealVector, Value = F> + Sync,
Term: TerminationCriterion<RealVector, F>,
{
pub fn build(self) -> Result<ContinuousUMDA<F, Fit, Term>, EvolutionError> {
self.config.validate()?;
let bounds = self
.bounds
.ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
let fitness = self.fitness.ok_or_else(|| {
EvolutionError::Configuration("Fitness function must be specified".to_string())
})?;
let termination = self.termination.ok_or_else(|| {
EvolutionError::Configuration("Termination criterion must be specified".to_string())
})?;
Ok(ContinuousUMDA {
config: self.config,
bounds,
fitness,
termination,
_phantom: std::marker::PhantomData,
})
}
}
pub struct ContinuousUMDA<F, Fit, Term>
where
F: FitnessValue,
{
config: UMDAConfig,
bounds: MultiBounds,
fitness: Fit,
termination: Term,
_phantom: std::marker::PhantomData<F>,
}
impl<F, Fit, Term> ContinuousUMDA<F, Fit, Term>
where
F: FitnessValue + Send,
Fit: Fitness<Genome = RealVector, Value = F> + Sync,
Term: TerminationCriterion<RealVector, F>,
{
pub fn builder() -> UMDABuilder<RealVector, F, (), ()> {
UMDABuilder::new()
}
pub fn run<R: Rng>(
&self,
rng: &mut R,
) -> Result<EvolutionResult<RealVector, F>, EvolutionError> {
self.run_with_model(rng).map(|(result, _model)| result)
}
pub fn run_with_callback<R: Rng, Cb: FnMut(usize, f64) -> bool>(
&self,
rng: &mut R,
on_generation: Cb,
) -> Result<EvolutionResult<RealVector, F>, EvolutionError> {
self.run_with_model_cb(rng, on_generation)
.map(|(result, _model)| result)
}
pub fn run_with_model<R: Rng>(
&self,
rng: &mut R,
) -> Result<(EvolutionResult<RealVector, F>, ContinuousUnivariateModel), EvolutionError> {
self.run_with_model_cb(rng, |_generation, _best_fitness| true)
}
fn run_with_model_cb<R: Rng, Cb: FnMut(usize, f64) -> bool>(
&self,
rng: &mut R,
mut on_generation: Cb,
) -> Result<(EvolutionResult<RealVector, F>, ContinuousUnivariateModel), EvolutionError> {
let start_time = Instant::now();
let mut model = ContinuousUnivariateModel::from_bounds(&self.bounds);
let mut population: Population<RealVector, F> =
Population::random(self.config.population_size, &self.bounds, rng);
population.evaluate(&self.fitness);
let mut stats = EvolutionStats::new();
let mut evaluations = self.config.population_size;
let mut fitness_history: Vec<f64> = Vec::new();
let mut generation = 0usize;
let mut best = population
.best()
.ok_or(EvolutionError::EmptyPopulation)?
.clone();
let gen_stats = GenerationStats::from_population(&population, 0, evaluations);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
loop {
let state = EvolutionState {
generation,
evaluations,
best_fitness: best.fitness_value().to_f64(),
population: &population,
fitness_history: &fitness_history,
};
if self.termination.should_terminate(&state) {
stats.set_termination_reason(self.termination.reason());
break;
}
if !on_generation(generation, best.fitness_value().to_f64()) {
break;
}
let gen_start = Instant::now();
let select_count =
(self.config.population_size as f64 * self.config.selection_ratio).ceil() as usize;
let selected: Vec<&RealVector> = select_top_genomes(&population, select_count);
model.update(&selected, &self.config);
population = Population::with_capacity(self.config.population_size);
for _ in 0..self.config.population_size {
let genome = model.sample(&self.bounds, rng);
population.push(Individual::new(genome));
}
population.evaluate(&self.fitness);
evaluations += self.config.population_size;
if let Some(pop_best) = population.best() {
if pop_best.is_better_than(&best) {
best = pop_best.clone();
}
}
generation += 1;
population.set_generation(generation);
let timing = TimingStats::new().with_total(gen_start.elapsed());
let gen_stats = GenerationStats::from_population(&population, generation, evaluations)
.with_timing(timing);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
}
stats.set_runtime(start_time.elapsed());
let result =
EvolutionResult::new(best.genome, best.fitness.unwrap(), generation, evaluations)
.with_stats(stats);
Ok((result, model))
}
}
#[derive(Clone, Debug)]
pub struct BinaryUnivariateModel {
pub probabilities: Vec<f64>,
}
impl BinaryUnivariateModel {
pub fn uniform(dimension: usize) -> Self {
Self {
probabilities: vec![0.5; dimension],
}
}
pub fn update(&mut self, selected: &[&BitString], config: &UMDAConfig) {
let n = selected.len() as f64;
if n == 0.0 {
return;
}
for i in 0..self.probabilities.len() {
let ones: f64 = selected
.iter()
.filter(|g| g.bits().get(i).copied().unwrap_or(false))
.count() as f64;
let new_prob = ones / n;
let bounded_prob = new_prob.clamp(config.prob_bounds.0, config.prob_bounds.1);
self.probabilities[i] = config.learning_rate * bounded_prob
+ (1.0 - config.learning_rate) * self.probabilities[i];
}
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> BitString {
let bits: Vec<bool> = self
.probabilities
.iter()
.map(|p| {
let dist = Bernoulli::new(*p).unwrap_or(Bernoulli::new(0.5).unwrap());
dist.sample(rng)
})
.collect();
BitString::new(bits)
}
}
impl<F, Fit, Term> UMDABuilder<BitString, F, Fit, Term>
where
F: FitnessValue + Send,
Fit: Fitness<Genome = BitString, Value = F> + Sync,
Term: TerminationCriterion<BitString, F>,
{
pub fn build(self) -> Result<BinaryUMDA<F, Fit, Term>, EvolutionError> {
self.config.validate()?;
let bounds = self
.bounds
.ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
let fitness = self.fitness.ok_or_else(|| {
EvolutionError::Configuration("Fitness function must be specified".to_string())
})?;
let termination = self.termination.ok_or_else(|| {
EvolutionError::Configuration("Termination criterion must be specified".to_string())
})?;
Ok(BinaryUMDA {
config: self.config,
bounds,
fitness,
termination,
_phantom: std::marker::PhantomData,
})
}
}
pub struct BinaryUMDA<F, Fit, Term>
where
F: FitnessValue,
{
config: UMDAConfig,
bounds: MultiBounds,
fitness: Fit,
termination: Term,
_phantom: std::marker::PhantomData<F>,
}
impl<F, Fit, Term> BinaryUMDA<F, Fit, Term>
where
F: FitnessValue + Send,
Fit: Fitness<Genome = BitString, Value = F> + Sync,
Term: TerminationCriterion<BitString, F>,
{
pub fn builder() -> UMDABuilder<BitString, F, (), ()> {
UMDABuilder::new()
}
pub fn run<R: Rng>(
&self,
rng: &mut R,
) -> Result<EvolutionResult<BitString, F>, EvolutionError> {
self.run_with_model(rng).map(|(result, _model)| result)
}
pub fn run_with_model<R: Rng>(
&self,
rng: &mut R,
) -> Result<(EvolutionResult<BitString, F>, BinaryUnivariateModel), EvolutionError> {
let start_time = Instant::now();
let dimension = self.bounds.dimension();
let mut model = BinaryUnivariateModel::uniform(dimension);
let mut population: Population<BitString, F> =
Population::random(self.config.population_size, &self.bounds, rng);
population.evaluate(&self.fitness);
let mut stats = EvolutionStats::new();
let mut evaluations = self.config.population_size;
let mut fitness_history: Vec<f64> = Vec::new();
let mut generation = 0usize;
let mut best = population
.best()
.ok_or(EvolutionError::EmptyPopulation)?
.clone();
let gen_stats = GenerationStats::from_population(&population, 0, evaluations);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
loop {
let state = EvolutionState {
generation,
evaluations,
best_fitness: best.fitness_value().to_f64(),
population: &population,
fitness_history: &fitness_history,
};
if self.termination.should_terminate(&state) {
stats.set_termination_reason(self.termination.reason());
break;
}
let gen_start = Instant::now();
let select_count =
(self.config.population_size as f64 * self.config.selection_ratio).ceil() as usize;
let selected: Vec<&BitString> = select_top_genomes(&population, select_count);
model.update(&selected, &self.config);
population = Population::with_capacity(self.config.population_size);
for _ in 0..self.config.population_size {
let genome: BitString = model.sample(rng);
population.push(Individual::new(genome));
}
population.evaluate(&self.fitness);
evaluations += self.config.population_size;
if let Some(pop_best) = population.best() {
if pop_best.is_better_than(&best) {
best = pop_best.clone();
}
}
generation += 1;
population.set_generation(generation);
let timing = TimingStats::new().with_total(gen_start.elapsed());
let gen_stats = GenerationStats::from_population(&population, generation, evaluations)
.with_timing(timing);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
}
stats.set_runtime(start_time.elapsed());
let result =
EvolutionResult::new(best.genome, best.fitness.unwrap(), generation, evaluations)
.with_stats(stats);
Ok((result, model))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fitness::benchmarks::{OneMax, Sphere};
use crate::genome::bounds::Bounds;
use crate::termination::MaxEvaluations;
use rand::SeedableRng;
#[test]
fn test_continuous_umda_builder() {
let bounds = MultiBounds::symmetric(5.0, 10);
let umda: Result<ContinuousUMDA<f64, _, _>, _> = UMDABuilder::new()
.population_size(50)
.selection_ratio(0.3)
.bounds(bounds)
.fitness(Sphere::new(10))
.max_generations(10)
.build();
assert!(umda.is_ok());
}
fn random_search_sphere_best<R: Rng>(
dim: usize,
half_width: f64,
budget: usize,
rng: &mut R,
) -> f64 {
let sphere = Sphere::new(dim);
let mut best = f64::NEG_INFINITY;
for _ in 0..budget {
let genes: Vec<f64> = (0..dim)
.map(|_| rng.gen_range(-half_width..half_width))
.collect();
let f = sphere.evaluate(&RealVector::new(genes));
if f > best {
best = f;
}
}
best
}
fn random_search_onemax_best<R: Rng>(dim: usize, budget: usize, rng: &mut R) -> usize {
let onemax = OneMax::new(dim);
let mut best = 0usize;
for _ in 0..budget {
let bits: Vec<bool> = (0..dim).map(|_| rng.gen::<bool>()).collect();
let f = onemax.evaluate(&BitString::new(bits));
if f > best {
best = f;
}
}
best
}
#[test]
fn test_continuous_umda_beats_random_search() {
let budget = 5000;
let dim = 10;
let half_width = 5.12;
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let umda: ContinuousUMDA<f64, _, _> = UMDABuilder::new()
.population_size(100)
.selection_ratio(0.3)
.min_variance(0.001)
.bounds(MultiBounds::symmetric(half_width, dim))
.fitness(Sphere::new(dim))
.termination(MaxEvaluations::new(budget))
.build()
.unwrap();
let (result, model) = umda.run_with_model(&mut rng).unwrap();
let mut baseline_rng = rand::rngs::StdRng::seed_from_u64(42);
let random_best = random_search_sphere_best(dim, half_width, budget, &mut baseline_rng);
assert!(
result.best_fitness > random_best + 5.0,
"UMDA best {} should beat random search {} by a clear margin",
result.best_fitness,
random_best
);
assert!(
result.best_fitness > -2.0,
"UMDA should get close to the optimum, got {}",
result.best_fitness
);
for (i, m) in model.means.iter().enumerate() {
assert!(
m.abs() < 0.5,
"learned mean[{i}] = {m} did not converge toward the optimum (0)"
);
}
}
#[test]
fn test_binary_umda_beats_random_search() {
let budget = 3000;
let dim = 20;
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
let umda: BinaryUMDA<usize, _, _> = UMDABuilder::new()
.population_size(100)
.selection_ratio(0.3)
.prob_bounds(0.02, 0.98)
.bounds(MultiBounds::uniform(Bounds::unit(), dim))
.fitness(OneMax::new(dim))
.termination(MaxEvaluations::new(budget))
.build()
.unwrap();
let (result, model) = umda.run_with_model(&mut rng).unwrap();
let mut baseline_rng = rand::rngs::StdRng::seed_from_u64(7);
let random_best = random_search_onemax_best(dim, budget, &mut baseline_rng);
assert!(
result.best_fitness > random_best,
"UMDA best {} should beat random search {}",
result.best_fitness,
random_best
);
assert!(
result.best_fitness >= 19,
"UMDA should nearly solve OneMax, got {}",
result.best_fitness
);
for (i, p) in model.probabilities.iter().enumerate() {
assert!(
*p > 0.8,
"learned probability[{i}] = {p} did not converge toward 1"
);
}
}
#[test]
fn test_umda_builder_validation_rejects_out_of_range() {
let bounds = MultiBounds::symmetric(5.0, 4);
let bad_ratio: Result<ContinuousUMDA<f64, _, _>, _> = UMDABuilder::new()
.population_size(50)
.selection_ratio(1.5)
.bounds(bounds.clone())
.fitness(Sphere::new(4))
.max_generations(5)
.build();
assert!(bad_ratio.is_err(), "selection_ratio 1.5 must be rejected");
let zero_lr: Result<ContinuousUMDA<f64, _, _>, _> = UMDABuilder::new()
.population_size(50)
.learning_rate(0.0)
.bounds(bounds.clone())
.fitness(Sphere::new(4))
.max_generations(5)
.build();
assert!(
zero_lr.is_err(),
"learning_rate 0.0 (a no-op) must be rejected"
);
let bad_probs: Result<BinaryUMDA<usize, _, _>, _> = UMDABuilder::new()
.population_size(50)
.prob_bounds(0.6, 0.4)
.bounds(MultiBounds::uniform(Bounds::unit(), 4))
.fitness(OneMax::new(4))
.max_generations(5)
.build();
assert!(
bad_probs.is_err(),
"prob_bounds with min > max must be rejected"
);
let ok: Result<ContinuousUMDA<f64, _, _>, _> = UMDABuilder::new()
.population_size(50)
.selection_ratio(0.3)
.learning_rate(0.5)
.bounds(bounds)
.fitness(Sphere::new(4))
.max_generations(5)
.build();
assert!(ok.is_ok(), "a valid configuration must still build");
}
#[test]
fn test_continuous_variance_is_bessel_corrected() {
let bounds = MultiBounds::symmetric(5.0, 1);
let mut model = ContinuousUnivariateModel::from_bounds(&bounds);
let config = UMDAConfig {
learning_rate: 1.0,
min_variance: 1e-12, ..Default::default()
};
let g1 = RealVector::new(vec![1.0]);
let g2 = RealVector::new(vec![2.0]);
let g3 = RealVector::new(vec![3.0]);
model.update(&[&g1, &g2, &g3], &config);
assert!(
(model.variances[0] - 1.0).abs() < 1e-9,
"expected Bessel-corrected variance 1.0, got {}",
model.variances[0]
);
}
#[test]
fn test_continuous_model_update() {
let bounds = MultiBounds::symmetric(5.0, 3);
let mut model = ContinuousUnivariateModel::from_bounds(&bounds);
let config = UMDAConfig::default();
let g1 = RealVector::new(vec![1.0, 2.0, 3.0]);
let g2 = RealVector::new(vec![2.0, 3.0, 4.0]);
let g3 = RealVector::new(vec![3.0, 4.0, 5.0]);
let selected = vec![&g1, &g2, &g3];
model.update(&selected, &config);
assert!((model.means[0] - 2.0).abs() < 0.01);
assert!((model.means[1] - 3.0).abs() < 0.01);
assert!((model.means[2] - 4.0).abs() < 0.01);
}
#[test]
fn test_binary_model_update() {
let mut model = BinaryUnivariateModel::uniform(4);
let config = UMDAConfig::default();
let g1 = BitString::new(vec![true, true, false, false]);
let g2 = BitString::new(vec![true, false, true, false]);
let g3 = BitString::new(vec![true, false, false, true]);
let selected = vec![&g1, &g2, &g3];
model.update(&selected, &config);
assert!(model.probabilities[0] > 0.9);
assert!(model.probabilities[1] > 0.2 && model.probabilities[1] < 0.5);
}
#[test]
fn test_learning_rate() {
let bounds = MultiBounds::symmetric(5.0, 2);
let mut model = ContinuousUnivariateModel::from_bounds(&bounds);
let config = UMDAConfig {
learning_rate: 0.5,
..Default::default()
};
let initial_mean = model.means[0];
let g1 = RealVector::new(vec![2.0, 2.0]);
let selected = vec![&g1];
model.update(&selected, &config);
assert!((model.means[0] - (0.5 * 2.0 + 0.5 * initial_mean)).abs() < 0.01);
}
#[test]
fn test_sample_rejection_avoids_boundary_pileup() {
let bounds = MultiBounds::symmetric(5.0, 1); let mut model = ContinuousUnivariateModel::from_bounds(&bounds);
model.means[0] = 5.0; model.variances[0] = 1.0;
let mut rng = rand::rngs::StdRng::seed_from_u64(123);
let n = 2000;
let on_boundary = (0..n)
.filter(|_| {
let g = model.sample(&bounds, &mut rng);
(g.genes()[0] - 5.0).abs() < 1e-12
})
.count();
assert!(
on_boundary <= 2,
"rejection sampling should not pile samples on the boundary, got {on_boundary}/{n}"
);
}
#[derive(Clone, Debug, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
struct LowerBetter(f64);
impl FitnessValue for LowerBetter {
fn to_f64(&self) -> f64 {
self.0
}
fn is_better_than(&self, other: &Self) -> bool {
self.0 < other.0
}
}
#[test]
fn test_select_top_uses_is_better_than() {
let mut pop: Population<RealVector, LowerBetter> = Population::new();
pop.push(Individual::with_fitness(
RealVector::new(vec![30.0]),
LowerBetter(3.0),
));
pop.push(Individual::with_fitness(
RealVector::new(vec![10.0]),
LowerBetter(1.0),
));
pop.push(Individual::with_fitness(
RealVector::new(vec![20.0]),
LowerBetter(2.0),
));
let top = select_top_genomes(&pop, 1);
assert_eq!(top.len(), 1);
assert_eq!(
top[0].genes()[0],
10.0,
"selection should pick the is_better_than-best (lowest) individual"
);
}
#[test]
fn test_umda_run_with_callback_reports_progress() {
let mut rng = rand::rngs::StdRng::seed_from_u64(3);
let umda: ContinuousUMDA<f64, _, _> = UMDABuilder::new()
.population_size(40)
.selection_ratio(0.3)
.bounds(MultiBounds::symmetric(5.12, 4))
.fitness(Sphere::new(4))
.max_generations(12)
.build()
.unwrap();
let mut seen: Vec<usize> = Vec::new();
let result = umda
.run_with_callback(&mut rng, |generation, best| {
assert!(best.is_finite());
seen.push(generation);
true
})
.unwrap();
assert_eq!(seen, (0..result.generations).collect::<Vec<_>>());
assert!(!seen.is_empty());
}
#[test]
fn test_umda_run_with_callback_cancels_early() {
let mut rng = rand::rngs::StdRng::seed_from_u64(4);
let umda: ContinuousUMDA<f64, _, _> = UMDABuilder::new()
.population_size(40)
.selection_ratio(0.3)
.bounds(MultiBounds::symmetric(5.12, 4))
.fitness(Sphere::new(4))
.max_generations(10_000)
.build()
.unwrap();
let mut calls = 0usize;
let result = umda
.run_with_callback(&mut rng, |_generation, _best| {
calls += 1;
calls < 6
})
.unwrap();
assert!(calls <= 6, "callback must stop being called after cancel");
assert!(
result.generations < 10,
"run must stop far short of the 10k budget, got {}",
result.generations
);
}
}