use std::time::Instant;
use rand::Rng;
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::permutation::Permutation;
use crate::genome::real_vector::RealVector;
use crate::genome::traits::EvolutionaryGenome;
use crate::hyperparameter::bayesian::{
ThompsonConfig, ThompsonSamplingTuner, TunableMutation, PARAM_CROSSOVER_PROB,
PARAM_MUTATION_RATE,
};
use crate::operators::crossover::{OxCrossover, SbxCrossover, UniformCrossover};
use crate::operators::mutation::{BitFlipMutation, PermutationSwapMutation, PolynomialMutation};
use crate::operators::selection::TournamentSelection;
use crate::operators::traits::{
BoundedCrossoverOperator, BoundedMutationOperator, CrossoverOperator, MutationOperator,
SelectionOperator,
};
use crate::population::individual::Individual;
use crate::population::population::Population;
use crate::termination::{EvolutionState, MaxGenerations, TerminationCriterion};
fn validate_config(config: &SimpleGAConfig, bounds: &MultiBounds) -> Result<(), EvolutionError> {
if config.population_size == 0 {
return Err(EvolutionError::Configuration(
"population_size must be at least 1".to_string(),
));
}
if bounds.dimension() == 0 {
return Err(EvolutionError::Configuration(
"bounds must have at least one dimension".to_string(),
));
}
if config.elitism && config.elite_count > config.population_size {
return Err(EvolutionError::Configuration(format!(
"elite_count ({}) cannot exceed population_size ({})",
config.elite_count, config.population_size
)));
}
if !(0.0..=1.0).contains(&config.crossover_probability) {
return Err(EvolutionError::Configuration(format!(
"crossover_probability must be in [0, 1], got {}",
config.crossover_probability
)));
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct SimpleGAConfig {
pub population_size: usize,
pub elitism: bool,
pub elite_count: usize,
pub crossover_probability: f64,
pub parallel_evaluation: bool,
}
impl Default for SimpleGAConfig {
fn default() -> Self {
Self {
population_size: 100,
elitism: true,
elite_count: 1,
crossover_probability: 0.9,
parallel_evaluation: true,
}
}
}
pub struct SimpleGABuilder<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
config: SimpleGAConfig,
bounds: Option<MultiBounds>,
selection: Option<S>,
crossover: Option<C>,
mutation: Option<M>,
fitness: Option<Fit>,
termination: Option<Term>,
adaptive: Option<ThompsonConfig>,
_phantom: std::marker::PhantomData<(G, F)>,
}
impl<G, F> SimpleGABuilder<G, F, (), (), (), (), ()>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub fn new() -> Self {
Self {
config: SimpleGAConfig::default(),
bounds: None,
selection: None,
crossover: None,
mutation: None,
fitness: None,
termination: None,
adaptive: None,
_phantom: std::marker::PhantomData,
}
}
}
impl
SimpleGABuilder<RealVector, f64, TournamentSelection, SbxCrossover, PolynomialMutation, (), ()>
{
pub fn real_valued() -> Self {
SimpleGABuilder::new()
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
}
}
impl
SimpleGABuilder<
BitString,
usize,
TournamentSelection,
UniformCrossover,
BitFlipMutation,
(),
(),
>
{
pub fn bit_string() -> Self {
SimpleGABuilder::new()
.selection(TournamentSelection::new(3))
.crossover(UniformCrossover::new())
.mutation(BitFlipMutation::new())
}
}
impl
SimpleGABuilder<
Permutation,
f64,
TournamentSelection,
OxCrossover,
PermutationSwapMutation,
(),
(),
>
{
pub fn permutation() -> Self {
SimpleGABuilder::new()
.selection(TournamentSelection::new(3))
.crossover(OxCrossover)
.mutation(PermutationSwapMutation::default())
}
}
impl<G, F> Default for SimpleGABuilder<G, F, (), (), (), (), ()>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
fn default() -> Self {
Self::new()
}
}
impl<G, F, S, C, M, Fit, Term> SimpleGABuilder<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub fn population_size(mut self, size: usize) -> Self {
self.config.population_size = size;
self
}
pub fn elitism(mut self, enabled: bool) -> Self {
self.config.elitism = enabled;
self
}
pub fn elite_count(mut self, count: usize) -> Self {
self.config.elite_count = count;
self
}
pub fn crossover_probability(mut self, probability: f64) -> Self {
self.config.crossover_probability = probability;
self
}
pub fn parallel_evaluation(mut self, enabled: bool) -> Self {
self.config.parallel_evaluation = enabled;
self
}
pub fn bounds(mut self, bounds: MultiBounds) -> Self {
self.bounds = Some(bounds);
self
}
pub fn adaptive_operators(mut self, config: ThompsonConfig) -> Self {
self.adaptive = Some(config);
self
}
pub fn selection<NewS>(self, selection: NewS) -> SimpleGABuilder<G, F, NewS, C, M, Fit, Term>
where
NewS: SelectionOperator<G>,
{
SimpleGABuilder {
config: self.config,
bounds: self.bounds,
selection: Some(selection),
adaptive: self.adaptive,
crossover: self.crossover,
mutation: self.mutation,
fitness: self.fitness,
termination: self.termination,
_phantom: std::marker::PhantomData,
}
}
pub fn crossover<NewC>(self, crossover: NewC) -> SimpleGABuilder<G, F, S, NewC, M, Fit, Term>
where
NewC: CrossoverOperator<G>,
{
SimpleGABuilder {
config: self.config,
bounds: self.bounds,
selection: self.selection,
crossover: Some(crossover),
adaptive: self.adaptive,
mutation: self.mutation,
fitness: self.fitness,
termination: self.termination,
_phantom: std::marker::PhantomData,
}
}
pub fn mutation<NewM>(self, mutation: NewM) -> SimpleGABuilder<G, F, S, C, NewM, Fit, Term>
where
NewM: MutationOperator<G>,
{
SimpleGABuilder {
config: self.config,
bounds: self.bounds,
selection: self.selection,
crossover: self.crossover,
mutation: Some(mutation),
adaptive: self.adaptive,
fitness: self.fitness,
termination: self.termination,
_phantom: std::marker::PhantomData,
}
}
pub fn fitness<NewFit>(self, fitness: NewFit) -> SimpleGABuilder<G, F, S, C, M, NewFit, Term>
where
NewFit: Fitness<Genome = G, Value = F>,
{
SimpleGABuilder {
config: self.config,
bounds: self.bounds,
selection: self.selection,
crossover: self.crossover,
mutation: self.mutation,
fitness: Some(fitness),
adaptive: self.adaptive,
termination: self.termination,
_phantom: std::marker::PhantomData,
}
}
pub fn termination<NewTerm>(
self,
termination: NewTerm,
) -> SimpleGABuilder<G, F, S, C, M, Fit, NewTerm>
where
NewTerm: TerminationCriterion<G, F>,
{
SimpleGABuilder {
config: self.config,
bounds: self.bounds,
selection: self.selection,
crossover: self.crossover,
mutation: self.mutation,
fitness: self.fitness,
termination: Some(termination),
adaptive: self.adaptive,
_phantom: std::marker::PhantomData,
}
}
pub fn max_generations(
self,
max: usize,
) -> SimpleGABuilder<G, F, S, C, M, Fit, MaxGenerations> {
SimpleGABuilder {
config: self.config,
bounds: self.bounds,
selection: self.selection,
crossover: self.crossover,
mutation: self.mutation,
fitness: self.fitness,
termination: Some(MaxGenerations::new(max)),
adaptive: self.adaptive,
_phantom: std::marker::PhantomData,
}
}
}
#[cfg(feature = "parallel")]
impl<G, F, S, C, M, Fit, Term> SimpleGABuilder<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome + Send + Sync,
F: FitnessValue + Send,
S: SelectionOperator<G>,
C: CrossoverOperator<G>,
M: MutationOperator<G>,
Fit: Fitness<Genome = G, Value = F> + Sync,
Term: TerminationCriterion<G, F>,
{
#[allow(clippy::type_complexity)]
pub fn build(self) -> Result<SimpleGA<G, F, S, C, M, Fit, Term>, EvolutionError> {
let bounds = self
.bounds
.ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
let selection = self.selection.ok_or_else(|| {
EvolutionError::Configuration("Selection operator must be specified".to_string())
})?;
let crossover = self.crossover.ok_or_else(|| {
EvolutionError::Configuration("Crossover operator must be specified".to_string())
})?;
let mutation = self.mutation.ok_or_else(|| {
EvolutionError::Configuration("Mutation operator 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())
})?;
validate_config(&self.config, &bounds)?;
let tuner = self.adaptive.map(|cfg| cfg.build_tuner());
Ok(SimpleGA {
config: self.config,
bounds,
selection,
crossover,
mutation,
fitness,
termination,
tuner,
_phantom: std::marker::PhantomData,
})
}
}
#[cfg(not(feature = "parallel"))]
impl<G, F, S, C, M, Fit, Term> SimpleGABuilder<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
S: SelectionOperator<G>,
C: CrossoverOperator<G>,
M: MutationOperator<G>,
Fit: Fitness<Genome = G, Value = F>,
Term: TerminationCriterion<G, F>,
{
#[allow(clippy::type_complexity)]
pub fn build(self) -> Result<SimpleGA<G, F, S, C, M, Fit, Term>, EvolutionError> {
let bounds = self
.bounds
.ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
let selection = self.selection.ok_or_else(|| {
EvolutionError::Configuration("Selection operator must be specified".to_string())
})?;
let crossover = self.crossover.ok_or_else(|| {
EvolutionError::Configuration("Crossover operator must be specified".to_string())
})?;
let mutation = self.mutation.ok_or_else(|| {
EvolutionError::Configuration("Mutation operator 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())
})?;
validate_config(&self.config, &bounds)?;
let tuner = self.adaptive.map(|cfg| cfg.build_tuner());
Ok(SimpleGA {
config: self.config,
bounds,
selection,
crossover,
mutation,
fitness,
termination,
tuner,
_phantom: std::marker::PhantomData,
})
}
}
pub struct SimpleGA<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
config: SimpleGAConfig,
bounds: MultiBounds,
selection: S,
crossover: C,
mutation: M,
fitness: Fit,
termination: Term,
tuner: Option<ThompsonSamplingTuner>,
_phantom: std::marker::PhantomData<(G, F)>,
}
impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
S: SelectionOperator<G>,
C: CrossoverOperator<G>,
M: MutationOperator<G> + TunableMutation + Clone,
Fit: Fitness<Genome = G, Value = F>,
Term: TerminationCriterion<G, F>,
{
pub fn tuner(&self) -> Option<&ThompsonSamplingTuner> {
self.tuner.as_ref()
}
pub fn run_adaptive<R: Rng>(
&mut self,
rng: &mut R,
) -> Result<EvolutionResult<G, F>, EvolutionError> {
let start_time = Instant::now();
let mut tuner = self
.tuner
.take()
.unwrap_or_else(|| ThompsonConfig::default().build_tuner());
let mut population: Population<G, F> =
Population::random(self.config.population_size, &self.bounds, rng);
population.evaluate(&self.fitness);
let mut stats = EvolutionStats::new();
let mut evaluations = population.len();
let mut fitness_history: Vec<f64> = Vec::new();
let mut best_individual = 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);
tuner.snapshot(0);
loop {
let state = EvolutionState {
generation: population.generation(),
evaluations,
best_fitness: best_individual.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 next_generation = population.generation() + 1;
tuner.select_all(rng);
let crossover_prob = tuner
.selected(PARAM_CROSSOVER_PROB)
.unwrap_or(self.config.crossover_probability);
let mut mutation = self.mutation.clone();
if let Some(mutation_prob) = tuner.selected(PARAM_MUTATION_RATE) {
mutation.set_mutation_probability(mutation_prob);
}
let mut new_population: Population<G, F> =
Population::with_capacity(self.config.population_size);
if self.config.elitism {
let mut sorted = population.clone();
sorted.sort_by_fitness();
for i in 0..self.config.elite_count.min(sorted.len()) {
new_population.push(sorted[i].clone());
}
}
let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
while new_population.len() < self.config.population_size {
let p1 = self.selection.select(&selection_pool, rng);
let p2 = self.selection.select(&selection_pool, rng);
let parent1 = &selection_pool[p1].0;
let parent2 = &selection_pool[p2].0;
let parent_best = selection_pool[p1].1.max(selection_pool[p2].1);
let (mut child1, mut child2) = if rng.gen::<f64>() < crossover_prob {
match self.crossover.crossover(parent1, parent2, rng).genome() {
Some((c1, c2)) => (c1, c2),
None => (parent1.clone(), parent2.clone()),
}
} else {
(parent1.clone(), parent2.clone())
};
mutation.mutate(&mut child1, rng);
mutation.mutate(&mut child2, rng);
let f1 = self.fitness.evaluate(&child1);
tuner.observe(f1.to_f64() > parent_best);
evaluations += 1;
let mut ind1 = Individual::with_fitness(child1, f1);
ind1.birth_generation = next_generation;
new_population.push(ind1);
if new_population.len() < self.config.population_size {
let f2 = self.fitness.evaluate(&child2);
tuner.observe(f2.to_f64() > parent_best);
evaluations += 1;
let mut ind2 = Individual::with_fitness(child2, f2);
ind2.birth_generation = next_generation;
new_population.push(ind2);
}
}
while new_population.len() > self.config.population_size {
new_population.pop();
}
new_population.set_generation(next_generation);
population = new_population;
if let Some(best) = population.best() {
if best.is_better_than(&best_individual) {
best_individual = best.clone();
}
}
let gen_stats =
GenerationStats::from_population(&population, population.generation(), evaluations);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
tuner.snapshot(next_generation);
}
stats.set_runtime(start_time.elapsed());
self.tuner = Some(tuner);
Ok(EvolutionResult::new(
best_individual.genome,
best_individual.fitness.unwrap(),
population.generation(),
evaluations,
)
.with_stats(stats))
}
}
#[cfg(feature = "parallel")]
impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome + Send + Sync,
F: FitnessValue + Send,
S: SelectionOperator<G>,
C: CrossoverOperator<G>,
M: MutationOperator<G>,
Fit: Fitness<Genome = G, Value = F> + Sync,
Term: TerminationCriterion<G, F>,
{
pub fn builder() -> SimpleGABuilder<G, F, (), (), (), (), ()> {
SimpleGABuilder::new()
}
pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
let start_time = Instant::now();
let mut population: Population<G, F> =
Population::random(self.config.population_size, &self.bounds, rng);
let eval_start = Instant::now();
if self.config.parallel_evaluation {
population.evaluate_parallel(&self.fitness);
} else {
population.evaluate(&self.fitness);
}
let eval_time = eval_start.elapsed();
let mut stats = EvolutionStats::new();
let mut evaluations = population.len();
let mut fitness_history: Vec<f64> = Vec::new();
let mut best_individual = population
.best()
.ok_or(EvolutionError::EmptyPopulation)?
.clone();
let gen_stats = GenerationStats::from_population(&population, 0, evaluations)
.with_timing(TimingStats::new().with_evaluation(eval_time));
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
loop {
let state = EvolutionState {
generation: population.generation(),
evaluations,
best_fitness: best_individual.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 mut new_population: Population<G, F> =
Population::with_capacity(self.config.population_size);
if self.config.elitism {
let mut sorted = population.clone();
sorted.sort_by_fitness();
for i in 0..self.config.elite_count.min(sorted.len()) {
new_population.push(sorted[i].clone());
}
}
let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
let _selection_start = Instant::now();
let mut selection_time = std::time::Duration::ZERO;
let mut crossover_time = std::time::Duration::ZERO;
let mut mutation_time = std::time::Duration::ZERO;
while new_population.len() < self.config.population_size {
let sel_start = Instant::now();
let parent1_idx = self.selection.select(&selection_pool, rng);
let parent2_idx = self.selection.select(&selection_pool, rng);
selection_time += sel_start.elapsed();
let parent1 = &selection_pool[parent1_idx].0;
let parent2 = &selection_pool[parent2_idx].0;
let cross_start = Instant::now();
let (mut child1, mut child2) =
if rng.gen::<f64>() < self.config.crossover_probability {
match self.crossover.crossover(parent1, parent2, rng).genome() {
Some((c1, c2)) => (c1, c2),
None => (parent1.clone(), parent2.clone()),
}
} else {
(parent1.clone(), parent2.clone())
};
crossover_time += cross_start.elapsed();
let mut_start = Instant::now();
self.mutation.mutate(&mut child1, rng);
self.mutation.mutate(&mut child2, rng);
mutation_time += mut_start.elapsed();
new_population.push(Individual::with_generation(
child1,
population.generation() + 1,
));
if new_population.len() < self.config.population_size {
new_population.push(Individual::with_generation(
child2,
population.generation() + 1,
));
}
}
while new_population.len() > self.config.population_size {
new_population.pop();
}
let eval_start = Instant::now();
if self.config.parallel_evaluation {
new_population.evaluate_parallel(&self.fitness);
} else {
new_population.evaluate(&self.fitness);
}
let eval_time = eval_start.elapsed();
evaluations += new_population.len()
- (if self.config.elitism {
self.config.elite_count
} else {
0
});
new_population.set_generation(population.generation() + 1);
population = new_population;
if let Some(best) = population.best() {
if best.is_better_than(&best_individual) {
best_individual = best.clone();
}
}
let timing = TimingStats::new()
.with_selection(selection_time)
.with_crossover(crossover_time)
.with_mutation(mutation_time)
.with_evaluation(eval_time)
.with_total(gen_start.elapsed());
let gen_stats =
GenerationStats::from_population(&population, population.generation(), evaluations)
.with_timing(timing);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
}
stats.set_runtime(start_time.elapsed());
Ok(EvolutionResult::new(
best_individual.genome,
best_individual.fitness.unwrap(),
population.generation(),
evaluations,
)
.with_stats(stats))
}
}
#[cfg(feature = "parallel")]
impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome + Send + Sync,
F: FitnessValue + Send,
S: SelectionOperator<G>,
C: BoundedCrossoverOperator<G>,
M: BoundedMutationOperator<G>,
Fit: Fitness<Genome = G, Value = F> + Sync,
Term: TerminationCriterion<G, F>,
{
pub fn run_bounded<R: Rng>(
&self,
rng: &mut R,
) -> Result<EvolutionResult<G, F>, EvolutionError> {
let start_time = Instant::now();
let mut population: Population<G, F> =
Population::random(self.config.population_size, &self.bounds, rng);
if self.config.parallel_evaluation {
population.evaluate_parallel(&self.fitness);
} else {
population.evaluate(&self.fitness);
}
let mut stats = EvolutionStats::new();
let mut evaluations = population.len();
let mut fitness_history: Vec<f64> = Vec::new();
let mut best_individual = 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: population.generation(),
evaluations,
best_fitness: best_individual.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 mut new_population: Population<G, F> =
Population::with_capacity(self.config.population_size);
if self.config.elitism {
let mut sorted = population.clone();
sorted.sort_by_fitness();
for i in 0..self.config.elite_count.min(sorted.len()) {
new_population.push(sorted[i].clone());
}
}
let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
while new_population.len() < self.config.population_size {
let parent1_idx = self.selection.select(&selection_pool, rng);
let parent2_idx = self.selection.select(&selection_pool, rng);
let parent1 = &selection_pool[parent1_idx].0;
let parent2 = &selection_pool[parent2_idx].0;
let (mut child1, mut child2) =
if rng.gen::<f64>() < self.config.crossover_probability {
match self
.crossover
.crossover_bounded(parent1, parent2, &self.bounds, rng)
.genome()
{
Some((c1, c2)) => (c1, c2),
None => (parent1.clone(), parent2.clone()),
}
} else {
(parent1.clone(), parent2.clone())
};
self.mutation.mutate_bounded(&mut child1, &self.bounds, rng);
self.mutation.mutate_bounded(&mut child2, &self.bounds, rng);
new_population.push(Individual::with_generation(
child1,
population.generation() + 1,
));
if new_population.len() < self.config.population_size {
new_population.push(Individual::with_generation(
child2,
population.generation() + 1,
));
}
}
while new_population.len() > self.config.population_size {
new_population.pop();
}
if self.config.parallel_evaluation {
new_population.evaluate_parallel(&self.fitness);
} else {
new_population.evaluate(&self.fitness);
}
evaluations += new_population.len()
- (if self.config.elitism {
self.config.elite_count
} else {
0
});
new_population.set_generation(population.generation() + 1);
population = new_population;
if let Some(best) = population.best() {
if best.is_better_than(&best_individual) {
best_individual = best.clone();
}
}
let gen_stats =
GenerationStats::from_population(&population, population.generation(), evaluations);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
}
stats.set_runtime(start_time.elapsed());
Ok(EvolutionResult::new(
best_individual.genome,
best_individual.fitness.unwrap(),
population.generation(),
evaluations,
)
.with_stats(stats))
}
}
#[cfg(not(feature = "parallel"))]
impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
S: SelectionOperator<G>,
C: CrossoverOperator<G>,
M: MutationOperator<G>,
Fit: Fitness<Genome = G, Value = F>,
Term: TerminationCriterion<G, F>,
{
pub fn builder() -> SimpleGABuilder<G, F, (), (), (), (), ()> {
SimpleGABuilder::new()
}
pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
let mut state = self.init_run(rng)?;
while self.step_generation(&mut state, rng)? {}
Ok(self.finish_run(state))
}
}
impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
S: SelectionOperator<G>,
C: CrossoverOperator<G>,
M: MutationOperator<G>,
Fit: Fitness<Genome = G, Value = F>,
Term: TerminationCriterion<G, F>,
{
pub fn init_run<R: Rng>(&self, rng: &mut R) -> Result<SimpleGaRun<G, F>, EvolutionError> {
let start_time = Instant::now();
let mut population: Population<G, F> =
Population::random(self.config.population_size, &self.bounds, rng);
let eval_start = Instant::now();
population.evaluate(&self.fitness);
let eval_time = eval_start.elapsed();
let mut stats = EvolutionStats::new();
let evaluations = population.len();
let mut fitness_history: Vec<f64> = Vec::new();
let best_individual = population
.best()
.ok_or(EvolutionError::EmptyPopulation)?
.clone();
let gen_stats = GenerationStats::from_population(&population, 0, evaluations)
.with_timing(TimingStats::new().with_evaluation(eval_time));
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
Ok(SimpleGaRun {
population,
best_individual,
evaluations,
fitness_history,
stats,
start_time,
terminated: false,
})
}
pub fn step_generation<R: Rng>(
&self,
state: &mut SimpleGaRun<G, F>,
rng: &mut R,
) -> Result<bool, EvolutionError> {
if state.terminated {
return Ok(false);
}
let evo_state = EvolutionState {
generation: state.population.generation(),
evaluations: state.evaluations,
best_fitness: state.best_individual.fitness_value().to_f64(),
population: &state.population,
fitness_history: &state.fitness_history,
};
if self.termination.should_terminate(&evo_state) {
state
.stats
.set_termination_reason(self.termination.reason());
state.terminated = true;
return Ok(false);
}
let gen_start = Instant::now();
let mut new_population: Population<G, F> =
Population::with_capacity(self.config.population_size);
if self.config.elitism {
let mut sorted = state.population.clone();
sorted.sort_by_fitness();
for i in 0..self.config.elite_count.min(sorted.len()) {
new_population.push(sorted[i].clone());
}
}
let selection_pool: Vec<(G, f64)> = state.population.as_fitness_pairs();
let _selection_start = Instant::now();
let mut selection_time = std::time::Duration::ZERO;
let mut crossover_time = std::time::Duration::ZERO;
let mut mutation_time = std::time::Duration::ZERO;
while new_population.len() < self.config.population_size {
let sel_start = Instant::now();
let parent1_idx = self.selection.select(&selection_pool, rng);
let parent2_idx = self.selection.select(&selection_pool, rng);
selection_time += sel_start.elapsed();
let parent1 = &selection_pool[parent1_idx].0;
let parent2 = &selection_pool[parent2_idx].0;
let cross_start = Instant::now();
let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
match self.crossover.crossover(parent1, parent2, rng).genome() {
Some((c1, c2)) => (c1, c2),
None => (parent1.clone(), parent2.clone()),
}
} else {
(parent1.clone(), parent2.clone())
};
crossover_time += cross_start.elapsed();
let mut_start = Instant::now();
self.mutation.mutate(&mut child1, rng);
self.mutation.mutate(&mut child2, rng);
mutation_time += mut_start.elapsed();
new_population.push(Individual::with_generation(
child1,
state.population.generation() + 1,
));
if new_population.len() < self.config.population_size {
new_population.push(Individual::with_generation(
child2,
state.population.generation() + 1,
));
}
}
while new_population.len() > self.config.population_size {
new_population.pop();
}
let eval_start = Instant::now();
new_population.evaluate(&self.fitness);
let eval_time = eval_start.elapsed();
state.evaluations += new_population.len()
- (if self.config.elitism {
self.config.elite_count
} else {
0
});
new_population.set_generation(state.population.generation() + 1);
state.population = new_population;
if let Some(best) = state.population.best() {
if best.is_better_than(&state.best_individual) {
state.best_individual = best.clone();
}
}
let timing = TimingStats::new()
.with_selection(selection_time)
.with_crossover(crossover_time)
.with_mutation(mutation_time)
.with_evaluation(eval_time)
.with_total(gen_start.elapsed());
let gen_stats = GenerationStats::from_population(
&state.population,
state.population.generation(),
state.evaluations,
)
.with_timing(timing);
state.fitness_history.push(gen_stats.best_fitness);
state.stats.record(gen_stats);
Ok(true)
}
pub fn finish_run(&self, mut state: SimpleGaRun<G, F>) -> EvolutionResult<G, F> {
state.stats.set_runtime(state.start_time.elapsed());
EvolutionResult::new(
state.best_individual.genome,
state
.best_individual
.fitness
.expect("best individual is always evaluated"),
state.population.generation(),
state.evaluations,
)
.with_stats(state.stats)
}
pub fn inject_migrants(&self, state: &mut SimpleGaRun<G, F>, migrants: Vec<G>) {
if migrants.is_empty() {
return;
}
state.population.sort_by_fitness();
let n = state.population.len();
if n == 0 {
return;
}
let k = migrants.len().min(n);
let evaluated: Vec<Individual<G, F>> = migrants
.into_iter()
.take(k)
.map(|genome| {
let value = self.fitness.evaluate(&genome);
Individual::with_fitness(genome, value)
})
.collect();
{
let individuals = state.population.individuals_mut();
for (i, individual) in evaluated.into_iter().enumerate() {
let idx = n - 1 - i; individuals[idx] = individual;
}
}
state.evaluations += k;
if let Some(best) = state.population.best() {
if best.is_better_than(&state.best_individual) {
state.best_individual = best.clone();
}
}
}
}
impl<G, S, C, M, Fit, Term> SimpleGA<G, f64, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
S: SelectionOperator<G>,
C: CrossoverOperator<G>,
M: MutationOperator<G>,
Fit: Fitness<Genome = G, Value = f64>,
Term: TerminationCriterion<G, f64>,
{
pub fn checkpoint_run<R>(
&self,
state: &SimpleGaRun<G, f64>,
rng: &R,
) -> Result<crate::checkpoint::Checkpoint<G>, crate::error::CheckpointError>
where
R: crate::checkpoint::SnapshotRng,
{
let individuals: Vec<Individual<G>> = state.population.iter().cloned().collect();
crate::checkpoint::Checkpoint::new(state.generation(), individuals)
.with_evaluations(state.evaluations())
.with_best(state.best_individual.clone())
.with_statistics(state.stats.generations.clone())
.with_rng(rng)
}
pub fn resume<R>(
&self,
checkpoint: &crate::checkpoint::Checkpoint<G>,
) -> Result<(SimpleGaRun<G, f64>, R), crate::error::CheckpointError>
where
R: crate::checkpoint::SnapshotRng,
{
let rng = checkpoint.restore_rng::<R>()?.ok_or_else(|| {
crate::error::CheckpointError::Corrupted(
"checkpoint has no captured RNG state; bit-identical resume requires a \
SnapshotRng captured via SimpleGA::checkpoint_run / Checkpoint::with_rng"
.to_string(),
)
})?;
let mut population: Population<G, f64> =
Population::with_capacity(checkpoint.population.len());
for ind in &checkpoint.population {
population.push(ind.clone());
}
population.set_generation(checkpoint.generation);
let best_individual = match &checkpoint.best {
Some(best) => best.clone(),
None => population.best().cloned().ok_or_else(|| {
crate::error::CheckpointError::Corrupted("empty population".to_string())
})?,
};
let mut stats = EvolutionStats::new();
stats.generations = checkpoint.statistics.clone();
let fitness_history: Vec<f64> = checkpoint
.statistics
.iter()
.map(|g| g.best_fitness)
.collect();
let state = SimpleGaRun {
population,
best_individual,
evaluations: checkpoint.evaluations,
fitness_history,
stats,
start_time: Instant::now(),
terminated: false,
};
Ok((state, rng))
}
pub fn run_from_checkpoint<R>(
&self,
checkpoint: &crate::checkpoint::Checkpoint<G>,
) -> Result<EvolutionResult<G, f64>, EvolutionError>
where
R: crate::checkpoint::SnapshotRng + Rng,
{
let (mut state, mut rng) = self.resume::<R>(checkpoint)?;
while self.step_generation(&mut state, &mut rng)? {}
Ok(self.finish_run(state))
}
}
pub struct SimpleGaRun<G, F = f64>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
population: Population<G, F>,
best_individual: Individual<G, F>,
evaluations: usize,
fitness_history: Vec<f64>,
stats: EvolutionStats,
start_time: Instant,
terminated: bool,
}
impl<G, F> SimpleGaRun<G, F>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub fn generation(&self) -> usize {
self.population.generation()
}
pub fn evaluations(&self) -> usize {
self.evaluations
}
pub fn best_fitness(&self) -> f64 {
self.best_individual.fitness_value().to_f64()
}
pub fn best_genome(&self) -> &G {
&self.best_individual.genome
}
pub fn fitness_history(&self) -> &[f64] {
&self.fitness_history
}
pub fn is_terminated(&self) -> bool {
self.terminated
}
pub fn best_genomes(&self, k: usize) -> Vec<G> {
let mut sorted = self.population.clone();
sorted.sort_by_fitness();
sorted
.iter()
.take(k)
.map(|ind| ind.genome.clone())
.collect()
}
}
#[cfg(not(feature = "parallel"))]
impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
S: SelectionOperator<G>,
C: BoundedCrossoverOperator<G>,
M: BoundedMutationOperator<G>,
Fit: Fitness<Genome = G, Value = F>,
Term: TerminationCriterion<G, F>,
{
pub fn run_bounded<R: Rng>(
&self,
rng: &mut R,
) -> Result<EvolutionResult<G, F>, EvolutionError> {
let start_time = Instant::now();
let mut population: Population<G, F> =
Population::random(self.config.population_size, &self.bounds, rng);
population.evaluate(&self.fitness);
let mut stats = EvolutionStats::new();
let mut evaluations = population.len();
let mut fitness_history: Vec<f64> = Vec::new();
let mut best_individual = 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: population.generation(),
evaluations,
best_fitness: best_individual.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 mut new_population: Population<G, F> =
Population::with_capacity(self.config.population_size);
if self.config.elitism {
let mut sorted = population.clone();
sorted.sort_by_fitness();
for i in 0..self.config.elite_count.min(sorted.len()) {
new_population.push(sorted[i].clone());
}
}
let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
while new_population.len() < self.config.population_size {
let parent1_idx = self.selection.select(&selection_pool, rng);
let parent2_idx = self.selection.select(&selection_pool, rng);
let parent1 = &selection_pool[parent1_idx].0;
let parent2 = &selection_pool[parent2_idx].0;
let (mut child1, mut child2) =
if rng.gen::<f64>() < self.config.crossover_probability {
match self
.crossover
.crossover_bounded(parent1, parent2, &self.bounds, rng)
.genome()
{
Some((c1, c2)) => (c1, c2),
None => (parent1.clone(), parent2.clone()),
}
} else {
(parent1.clone(), parent2.clone())
};
self.mutation.mutate_bounded(&mut child1, &self.bounds, rng);
self.mutation.mutate_bounded(&mut child2, &self.bounds, rng);
new_population.push(Individual::with_generation(
child1,
population.generation() + 1,
));
if new_population.len() < self.config.population_size {
new_population.push(Individual::with_generation(
child2,
population.generation() + 1,
));
}
}
while new_population.len() > self.config.population_size {
new_population.pop();
}
new_population.evaluate(&self.fitness);
evaluations += new_population.len()
- (if self.config.elitism {
self.config.elite_count
} else {
0
});
new_population.set_generation(population.generation() + 1);
population = new_population;
if let Some(best) = population.best() {
if best.is_better_than(&best_individual) {
best_individual = best.clone();
}
}
let gen_stats =
GenerationStats::from_population(&population, population.generation(), evaluations);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
}
stats.set_runtime(start_time.elapsed());
Ok(EvolutionResult::new(
best_individual.genome,
best_individual.fitness.unwrap(),
population.generation(),
evaluations,
)
.with_stats(stats))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fitness::benchmarks::{OneMax, Sphere};
use crate::genome::traits::RealValuedGenome;
use crate::operators::crossover::{SbxCrossover, UniformCrossover};
use crate::operators::mutation::{BitFlipMutation, GaussianMutation, PolynomialMutation};
use crate::operators::selection::TournamentSelection;
use crate::termination::TargetFitness;
#[test]
fn test_simple_ga_builder() {
let bounds = MultiBounds::symmetric(5.0, 10);
let ga = SimpleGABuilder::new()
.population_size(50)
.bounds(bounds)
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(10))
.max_generations(10)
.build();
assert!(ga.is_ok());
}
#[test]
fn test_simple_ga_missing_bounds() {
let ga = SimpleGABuilder::new()
.population_size(50)
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(10))
.max_generations(10)
.build();
assert!(ga.is_err());
if let Err(e) = ga {
assert!(e.to_string().contains("Bounds"));
}
}
#[test]
fn test_simple_ga_sphere() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 10);
let ga = SimpleGABuilder::new()
.population_size(50)
.bounds(bounds)
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(10))
.max_generations(100)
.build()
.unwrap();
let result = ga.run(&mut rng).unwrap();
assert!(
result.best_fitness > -200.0,
"Expected fitness > -200, got {}",
result.best_fitness
); assert!(result.generations <= 100);
assert!(result.evaluations > 0);
}
#[cfg(not(feature = "parallel"))]
#[test]
fn test_step_api_matches_run() {
use rand::SeedableRng;
let build = || {
SimpleGABuilder::new()
.population_size(30)
.bounds(MultiBounds::symmetric(5.12, 6))
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(6))
.max_generations(25)
.build()
.unwrap()
};
let ga_a = build();
let mut rng_a = rand::rngs::StdRng::seed_from_u64(2024);
let run_result = ga_a.run(&mut rng_a).unwrap();
let ga_b = build();
let mut rng_b = rand::rngs::StdRng::seed_from_u64(2024);
let mut state = ga_b.init_run(&mut rng_b).unwrap();
let mut steps = 0;
while ga_b.step_generation(&mut state, &mut rng_b).unwrap() {
steps += 1;
}
assert!(state.is_terminated());
assert_eq!(steps, 25, "should take exactly max_generations steps");
let step_result = ga_b.finish_run(state);
assert_eq!(run_result.generations, step_result.generations);
assert_eq!(run_result.evaluations, step_result.evaluations);
assert_eq!(run_result.best_fitness, step_result.best_fitness);
assert_eq!(
run_result.best_genome.genes(),
step_result.best_genome.genes()
);
}
#[cfg(not(feature = "parallel"))]
#[test]
fn test_inject_migrants_replaces_worst() {
use rand::SeedableRng;
let ga = SimpleGABuilder::new()
.population_size(20)
.bounds(MultiBounds::symmetric(5.12, 4))
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(4))
.max_generations(10)
.build()
.unwrap();
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
let mut state = ga.init_run(&mut rng).unwrap();
for _ in 0..3 {
ga.step_generation(&mut state, &mut rng).unwrap();
}
let evals_before = state.evaluations();
let optimum = RealVector::new(vec![0.0; 4]);
ga.inject_migrants(&mut state, vec![optimum]);
assert_eq!(state.evaluations(), evals_before + 1);
assert!(
state.best_fitness() > -1e-9,
"injected optimum should become the best (got {})",
state.best_fitness()
);
let top = state.best_genomes(2);
assert_eq!(top.len(), 2);
}
#[test]
fn test_simple_ga_onemax() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::uniform(crate::genome::bounds::Bounds::unit(), 20);
let ga = SimpleGABuilder::new()
.population_size(50)
.bounds(bounds)
.selection(TournamentSelection::new(3))
.crossover(UniformCrossover::new())
.mutation(BitFlipMutation::new())
.fitness(OneMax::new(20))
.max_generations(50)
.build()
.unwrap();
let result = ga.run(&mut rng).unwrap();
assert!(result.best_fitness >= 15); }
#[test]
fn test_simple_ga_target_fitness() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 5);
let ga = SimpleGABuilder::new()
.population_size(100)
.bounds(bounds)
.selection(TournamentSelection::new(5))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(5))
.termination(TargetFitness::with_tolerance(0.0, 0.1)) .build()
.unwrap();
let result = ga.run(&mut rng).unwrap();
assert!(result.best_fitness >= -0.1);
assert_eq!(
result.stats.termination_reason.as_deref(),
Some("Target fitness reached")
);
}
#[test]
fn test_simple_ga_bounded() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 10);
let ga = SimpleGABuilder::new()
.population_size(50)
.bounds(bounds.clone())
.selection(TournamentSelection::new(3))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(10))
.max_generations(50)
.build()
.unwrap();
let result = ga.run_bounded(&mut rng).unwrap();
for gene in result.best_genome.genes() {
assert!(*gene >= -5.12 && *gene <= 5.12);
}
}
#[test]
fn test_simple_ga_elitism() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 5);
let ga_no_elite = SimpleGABuilder::new()
.population_size(20)
.elitism(false)
.bounds(bounds.clone())
.selection(TournamentSelection::new(2))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(5))
.max_generations(20)
.build()
.unwrap();
let ga_elite = SimpleGABuilder::new()
.population_size(20)
.elitism(true)
.elite_count(2)
.bounds(bounds)
.selection(TournamentSelection::new(2))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(5))
.max_generations(20)
.build()
.unwrap();
let result_no_elite = ga_no_elite.run(&mut rng);
let result_elite = ga_elite.run(&mut rng);
assert!(result_no_elite.is_ok());
assert!(result_elite.is_ok());
}
#[test]
fn test_simple_ga_statistics() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 5);
let ga = SimpleGABuilder::new()
.population_size(20)
.bounds(bounds)
.selection(TournamentSelection::new(2))
.crossover(SbxCrossover::new(20.0))
.mutation(PolynomialMutation::new(20.0))
.fitness(Sphere::new(5))
.max_generations(10)
.build()
.unwrap();
let result = ga.run(&mut rng).unwrap();
assert_eq!(result.stats.num_generations(), 11); assert!(result.stats.total_runtime_ms > 0.0);
let history = result.stats.best_fitness_history();
for i in 1..history.len() {
assert!(history[i] >= history[i - 1] - 0.001); }
}
#[test]
fn test_real_valued_constructor_no_turbofish() {
let mut rng = rand::thread_rng();
let ga = SimpleGABuilder::real_valued()
.population_size(40)
.bounds(MultiBounds::symmetric(5.12, 10))
.fitness(Sphere::new(10))
.max_generations(20)
.build()
.unwrap();
let result = ga.run(&mut rng).unwrap();
assert!(result.evaluations > 0);
}
#[test]
fn test_real_valued_constructor_override_operator() {
let mut rng = rand::thread_rng();
let ga = SimpleGABuilder::real_valued()
.mutation(GaussianMutation::new(0.1))
.population_size(30)
.bounds(MultiBounds::symmetric(5.12, 5))
.fitness(Sphere::new(5))
.max_generations(10)
.build()
.unwrap();
assert!(ga.run(&mut rng).is_ok());
}
#[test]
fn test_bit_string_constructor_no_turbofish() {
let mut rng = rand::thread_rng();
let ga = SimpleGABuilder::bit_string()
.population_size(40)
.bounds(MultiBounds::uniform(
crate::genome::bounds::Bounds::unit(),
20,
))
.fitness(OneMax::new(20))
.max_generations(30)
.build()
.unwrap();
let result = ga.run(&mut rng).unwrap();
assert!(result.best_fitness >= 10);
}
#[test]
fn test_permutation_constructor_no_turbofish() {
use crate::fitness::traits::FnFitness;
use crate::genome::permutation::Permutation;
use crate::genome::traits::PermutationGenome;
let mut rng = rand::thread_rng();
let fitness = FnFitness::new(|p: &Permutation| -> f64 {
-(p.permutation()
.iter()
.enumerate()
.map(|(i, &v)| (v as f64 - i as f64).abs())
.sum::<f64>())
});
let ga = SimpleGABuilder::permutation()
.population_size(30)
.bounds(MultiBounds::symmetric(1.0, 8))
.fitness(fitness)
.max_generations(10)
.build()
.unwrap();
assert!(ga.run(&mut rng).is_ok());
}
#[test]
fn test_build_rejects_zero_population() {
let ga = SimpleGABuilder::real_valued()
.population_size(0)
.bounds(MultiBounds::symmetric(5.12, 10))
.fitness(Sphere::new(10))
.max_generations(10)
.build();
assert!(ga.is_err());
let msg = ga.err().unwrap().to_string();
assert!(msg.contains("population_size"), "got: {msg}");
}
#[test]
fn test_build_rejects_out_of_range_crossover_probability() {
let ga = SimpleGABuilder::real_valued()
.crossover_probability(1.5)
.population_size(20)
.bounds(MultiBounds::symmetric(5.12, 5))
.fitness(Sphere::new(5))
.max_generations(5)
.build();
assert!(ga.is_err());
assert!(ga
.err()
.unwrap()
.to_string()
.contains("crossover_probability"));
}
#[test]
fn test_build_rejects_elite_count_exceeding_population() {
let ga = SimpleGABuilder::real_valued()
.population_size(10)
.elite_count(50)
.bounds(MultiBounds::symmetric(5.12, 5))
.fitness(Sphere::new(5))
.max_generations(5)
.build();
assert!(ga.is_err());
assert!(ga.err().unwrap().to_string().contains("elite_count"));
}
#[test]
fn test_run_adaptive_feeds_tuner() {
use crate::hyperparameter::bayesian::{ThompsonConfig, PARAM_MUTATION_RATE};
let mut rng = rand::thread_rng();
let mut ga = SimpleGABuilder::real_valued()
.population_size(30)
.bounds(MultiBounds::symmetric(5.12, 8))
.fitness(Sphere::new(8))
.max_generations(15)
.adaptive_operators(ThompsonConfig::default())
.build()
.unwrap();
let result = ga.run_adaptive(&mut rng).unwrap();
assert!(result.evaluations > 0);
let tuner = ga
.tuner()
.expect("tuner should be present after run_adaptive");
assert!(
tuner.total_observations() > 0,
"tuner must receive improvement feedback"
);
let mr = tuner.parameter(PARAM_MUTATION_RATE).unwrap();
assert!(
mr.total_observations() > 0.0,
"mutation-rate arms must accumulate observations"
);
}
}