use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{debug, info};
use super::{
builder::EvolutionLauncherBuilder,
challenge::Challenge,
options::{EvolutionOptions, LogLevel},
};
use crate::{
breeding::BreedStrategy,
error::{GeneticError, Result},
local_search::{LocalSearchApplicationStrategy, LocalSearchManager},
phenotype::Phenotype,
rng::RandomNumberGenerator,
selection::SelectionStrategy,
LocalSearch, OptionExt,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct EvolutionResult<Pheno: Phenotype> {
pub pheno: Pheno,
pub score: f64,
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct EvolutionLauncher<P, B, S, LS, F, A>
where
P: Phenotype,
B: BreedStrategy<P>,
S: SelectionStrategy<P>,
LS: LocalSearch<P, F> + Clone,
F: Challenge<P> + Clone,
A: LocalSearchApplicationStrategy<P> + Clone,
{
breed_strategy: B,
selection_strategy: S,
local_search_manager: Option<LocalSearchManager<P, LS, A, F>>,
challenge: F,
}
impl<P, B, S, LS, F, A> EvolutionLauncher<P, B, S, LS, F, A>
where
P: Phenotype + Send + Sync,
LS: LocalSearch<P, F> + Clone + Send + Sync,
F: Challenge<P> + Clone + Send + Sync,
A: LocalSearchApplicationStrategy<P> + Clone + Send + Sync,
B: BreedStrategy<P> + Clone + Send + Sync,
S: SelectionStrategy<P> + Clone + Send + Sync,
{
pub fn new(
breed_strategy: B,
selection_strategy: S,
local_search_manager: Option<LocalSearchManager<P, LS, A, F>>,
challenge: F,
) -> Self {
Self {
breed_strategy,
selection_strategy,
local_search_manager,
challenge,
}
}
pub fn builder() -> EvolutionLauncherBuilder<P, B, S, LS, F, A> {
EvolutionLauncherBuilder::new()
}
pub fn configure(
&self,
options: EvolutionOptions,
starting_value: P,
) -> EvolutionProcess<P, B, S, LS, F, A> {
EvolutionProcess {
launcher: self,
options,
starting_value,
use_local_search: false,
seed: None,
}
}
fn evolve(
&self,
options: &EvolutionOptions,
rng: &mut RandomNumberGenerator,
starting_value: P,
use_local_search: bool,
) -> Result<EvolutionResult<P>> {
if options.get_population_size() == 0 {
return Err(GeneticError::Configuration(
"Population size cannot be zero".to_string(),
));
}
if options.get_num_offspring() == 0 {
return Err(GeneticError::Configuration(
"Number of offspring cannot be zero".to_string(),
));
}
let mut fitness: Vec<EvolutionResult<P>> = Vec::new();
let mut candidates: Vec<P> = Vec::new();
let mut parents: Vec<P> = vec![starting_value];
self.breed(&mut candidates, &parents, options, rng, 0)?;
for generation in 0..options.get_num_generations() {
let population: Vec<P> = candidates.to_vec();
if candidates.len() >= options.get_parallel_threshold() {
fitness = self.fitness_parallel(&candidates, &self.challenge)?;
} else {
fitness = self.fitness_sequential(&candidates, &self.challenge)?;
}
let scores: Vec<f64> = fitness.iter().map(|f| f.score).collect();
match options.get_log_level() {
LogLevel::Info => info!(generation, "Evolution progress"),
LogLevel::Debug => {
fitness.iter().for_each(|result| {
debug!(
generation,
phenotype = ?result.pheno,
score = result.score,
"Evolution detailed progress"
);
});
}
LogLevel::None => {}
}
parents = self.selection_strategy.select(
&population,
&scores,
options.get_population_size(),
)?;
self.breed(&mut candidates, &parents, options, rng, generation)?;
if use_local_search {
match &self.local_search_manager {
Some(manager) => {
manager.apply(&mut candidates, &scores, &self.challenge)?;
}
None => {
return Err(GeneticError::Configuration(
"Local search algorithm not provided".to_string(),
))
}
}
}
if fitness.is_empty() {
return Err(GeneticError::Evolution(format!(
"No viable candidates produced in generation {}",
generation
)));
}
}
fitness.sort_by(|a, b| {
b.score.partial_cmp(&a.score).unwrap_or_else(|| {
if b.score.is_nan() {
std::cmp::Ordering::Less
} else if a.score.is_nan() {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
})
});
fitness.first().cloned().ok_or_else_genetic(|| {
GeneticError::Evolution(
"Evolution completed but no viable candidates were produced".to_string(),
)
})
}
fn breed(
&self,
candidates: &mut Vec<P>,
parents: &[P],
options: &EvolutionOptions,
rng: &mut RandomNumberGenerator,
generation: usize,
) -> std::result::Result<(), GeneticError> {
match self.breed_strategy.breed(parents, options, rng) {
Ok(bred_candidates) => {
candidates.extend(bred_candidates);
Ok(())
}
Err(e) => Err(GeneticError::Breeding(format!(
"Failed to breed candidates in generation {}: {}",
generation, e
))),
}
}
fn fitness_parallel(&self, candidates: &[P], challenge: &F) -> Result<Vec<EvolutionResult<P>>> {
candidates
.par_iter()
.map(|candidate| {
let score = challenge.score(candidate);
if !score.is_finite() {
return Err(GeneticError::FitnessCalculation(format!(
"Non-finite fitness score encountered: {}",
score
)));
}
Ok(EvolutionResult {
pheno: candidate.clone(),
score,
})
})
.collect()
}
fn fitness_sequential(
&self,
candidates: &[P],
challenge: &F,
) -> Result<Vec<EvolutionResult<P>>> {
candidates
.iter()
.map(|candidate| {
let score = challenge.score(candidate);
if !score.is_finite() {
return Err(GeneticError::FitnessCalculation(format!(
"Non-finite fitness score encountered: {}",
score
)));
}
Ok(EvolutionResult {
pheno: candidate.clone(),
score,
})
})
.collect()
}
}
pub struct EvolutionProcess<'a, P, B, S, LS, F, A>
where
P: Phenotype,
B: BreedStrategy<P>,
S: SelectionStrategy<P>,
LS: LocalSearch<P, F> + Clone,
F: Challenge<P> + Clone,
A: LocalSearchApplicationStrategy<P> + Clone,
{
launcher: &'a EvolutionLauncher<P, B, S, LS, F, A>,
options: EvolutionOptions,
starting_value: P,
use_local_search: bool,
seed: Option<u64>,
}
impl<P, B, S, LS, F, A> EvolutionProcess<'_, P, B, S, LS, F, A>
where
P: Phenotype + Send + Sync,
LS: LocalSearch<P, F> + Clone + Send + Sync,
F: Challenge<P> + Clone + Send + Sync,
A: LocalSearchApplicationStrategy<P> + Clone + Send + Sync,
B: BreedStrategy<P> + Clone + Send + Sync,
S: SelectionStrategy<P> + Clone + Send + Sync,
{
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_local_search(mut self) -> Self {
self.use_local_search = true;
self
}
pub fn run(self) -> Result<EvolutionResult<P>> {
let mut rng = match self.seed {
Some(seed) => RandomNumberGenerator::from_seed(seed),
None => RandomNumberGenerator::new(),
};
let use_local_search = if self.launcher.local_search_manager.is_some() {
self.use_local_search
} else {
false
};
self.launcher.evolve(
&self.options,
&mut rng,
self.starting_value,
use_local_search,
)
}
}