use std::borrow::Cow;
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use rand::Rng;
use crate::configuration::ProblemSolving;
use crate::error::GaError;
use crate::ga::TerminationCause;
use crate::observer::GaObserver;
use crate::rng::make_rng;
use crate::stats::GenerationStats;
use crate::traits::{FitnessFn, GeneT, LinearChromosome, RealGene};
use super::configuration::EdaConfiguration;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EdaModel {
Bernoulli(Vec<f64>),
Gaussian {
means: Vec<f64>,
stds: Vec<f64>,
},
}
pub struct EdaResult<U: LinearChromosome> {
pub population: Vec<U>,
pub best: U,
pub best_fitness: f64,
pub generations: usize,
pub learned_model: EdaModel,
}
pub struct EdaEngine<U: LinearChromosome> {
config: EdaConfiguration,
init_fn: Arc<dyn Fn(usize) -> Vec<U> + Send + Sync>,
fitness_fn: Arc<FitnessFn<U::Gene>>,
observer: Option<Arc<dyn GaObserver<U> + Send + Sync>>,
fitness_cache: Option<Arc<Mutex<crate::fitness::cache::FitnessCache>>>,
}
impl<U: LinearChromosome + Clone> EdaEngine<U> {
pub fn new(
config: EdaConfiguration,
init_fn: impl Fn(usize) -> Vec<U> + Send + Sync + 'static,
fitness_fn: impl Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
) -> Self {
Self {
config,
init_fn: Arc::new(init_fn),
fitness_fn: Arc::new(fitness_fn),
observer: None,
fitness_cache: None,
}
}
pub fn bernoulli(
config: EdaConfiguration,
init_fn: impl Fn(usize) -> Vec<U> + Send + Sync + 'static,
fitness_fn: impl Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
) -> Self {
Self::new(config, init_fn, fitness_fn)
}
pub fn with_observer(mut self, obs: Arc<dyn GaObserver<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
fn notify<F: FnOnce(&dyn GaObserver<U>)>(&self, f: F) {
if let Some(ref obs) = self.observer {
f(obs.as_ref());
}
}
#[inline]
fn is_better(&self, candidate: f64, current: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => candidate < current,
ProblemSolving::Maximization => candidate > current,
ProblemSolving::FixedFitness => {
if let Some(t) = self.config.fitness_target {
(candidate - t).abs() < (current - t).abs()
} else {
candidate < current
}
}
}
}
fn reached_target(&self, fitness: f64, target: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => fitness <= target,
ProblemSolving::Maximization => fitness >= target,
ProblemSolving::FixedFitness => (fitness - target).abs() < 1e-6,
}
}
fn find_best(&self, pop: &[U]) -> (usize, f64) {
assert!(
!pop.is_empty(),
"EdaEngine::find_best called with empty population"
);
let mut best_idx = 0;
let mut best_fit = pop[0].fitness();
for (i, ind) in pop.iter().enumerate().skip(1) {
if self.is_better(ind.fitness(), best_fit) {
best_fit = ind.fitness();
best_idx = i;
}
}
(best_idx, best_fit)
}
fn sample_bernoulli<R: Rng>(template: &U, probs: &[f64], rng: &mut R) -> U {
let new_dna: Vec<U::Gene> = template
.dna()
.iter()
.zip(probs.iter())
.enumerate()
.map(|(i, (gene, &p))| {
let new_id = if rng.random::<f64>() < p { 1 } else { 0 };
let mut g = gene.clone();
g.set_id(new_id);
let _ = i; g
})
.collect();
let mut offspring = template.clone();
offspring.set_dna(Cow::Owned(new_dna));
offspring
}
pub fn run(&mut self) -> Result<EdaResult<U>, GaError>
where
U::Gene: Debug,
{
let mut rng = make_rng();
let is_maximization = matches!(self.config.problem_solving, ProblemSolving::Maximization);
if let Some(size) = self.config.fitness_cache_size {
if self.fitness_cache.is_none() {
let (wrapped_fn, cache_handle) =
crate::fitness::cache::wrap_with_cache(Arc::clone(&self.fitness_fn), size);
self.fitness_fn = wrapped_fn;
self.fitness_cache = Some(cache_handle);
}
}
let cmp = |a_fit: f64, b_fit: f64| -> std::cmp::Ordering {
match self.config.problem_solving {
ProblemSolving::Maximization => b_fit
.partial_cmp(&a_fit)
.unwrap_or(std::cmp::Ordering::Equal),
ProblemSolving::Minimization => a_fit
.partial_cmp(&b_fit)
.unwrap_or(std::cmp::Ordering::Equal),
ProblemSolving::FixedFitness => {
let t = self.config.fitness_target.unwrap_or(0.0);
let da = (a_fit - t).abs();
let db = (b_fit - t).abs();
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
}
}
};
self.notify(|obs| obs.on_run_start());
let pop_size = if self.config.population_size == 0 {
100
} else {
self.config.population_size
};
let mut pop: Vec<U> = (self.init_fn)(pop_size.max(1));
if pop.is_empty() {
return Err(GaError::InitializationError(
"EdaEngine: init_fn returned an empty population".to_string(),
));
}
for ind in &mut pop {
let f = (self.fitness_fn)(ind.dna());
ind.set_fitness(f);
}
let (best_idx, mut best_fitness) = self.find_best(&pop);
let mut best = pop[best_idx].clone();
let dim = pop[0].dna().len();
self.notify(|obs| obs.on_new_best(0, &best));
let mut termination_cause = TerminationCause::GenerationLimitReached;
let mut all_stats: Vec<GenerationStats> = Vec::with_capacity(self.config.max_generations);
let mut learned_model = EdaModel::Bernoulli(vec![0.5; dim]);
let mut best_model = learned_model.clone();
let (mut prev_cache_hits, mut prev_cache_misses) = match &self.fitness_cache {
Some(ch) => {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
(c.hits(), c.misses())
}
None => (0, 0),
};
for gen in 0..self.config.max_generations {
self.notify(|obs| obs.on_generation_start(gen));
let n_selected = ((pop_size as f64 * self.config.selection_ratio).floor() as usize)
.max(1)
.min(pop.len());
let mut indices: Vec<usize> = (0..pop.len()).collect();
indices.select_nth_unstable_by(n_selected - 1, |&a, &b| {
cmp(pop[a].fitness(), pop[b].fitness())
});
let selected: Vec<&U> = indices[..n_selected].iter().map(|&i| &pop[i]).collect();
let probs = Self::estimate_bernoulli_ref(&selected, dim);
learned_model = EdaModel::Bernoulli(probs.clone());
let template = pop[0].clone();
let mut new_pop: Vec<U> = (0..pop_size)
.map(|_| Self::sample_bernoulli(&template, &probs, &mut rng))
.collect();
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
{
use rayon::prelude::*;
let fitness_fn = Arc::clone(&self.fitness_fn);
let fitnesses: Vec<f64> = new_pop
.par_iter()
.map(|ind| fitness_fn(ind.dna()))
.collect();
for (ind, f) in new_pop.iter_mut().zip(fitnesses.into_iter()) {
ind.set_fitness(f);
}
}
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
{
for ind in &mut new_pop {
let f = (self.fitness_fn)(ind.dna());
ind.set_fitness(f);
}
}
pop = new_pop;
let (gen_best_idx, gen_best_fit) = self.find_best(&pop);
if self.is_better(gen_best_fit, best_fitness) {
best_fitness = gen_best_fit;
best = pop[gen_best_idx].clone();
best_model = learned_model.clone();
self.notify(|obs| obs.on_new_best(gen, &best));
}
let fitness_values: Vec<f64> = pop.iter().map(|c| c.fitness()).collect();
let mut stats =
GenerationStats::from_fitness_values(gen, &fitness_values, is_maximization);
if let Some(ref ch) = self.fitness_cache {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
stats.cache_hits = Some(c.hits().saturating_sub(prev_cache_hits));
stats.cache_misses = Some(c.misses().saturating_sub(prev_cache_misses));
prev_cache_hits = c.hits();
prev_cache_misses = c.misses();
}
self.notify(|obs| obs.on_generation_end(&stats));
all_stats.push(stats);
if let Some(target) = self.config.fitness_target {
if self.reached_target(best_fitness, target) {
termination_cause = TerminationCause::FitnessTargetReached;
break;
}
}
}
let generations = all_stats.len();
let all_stats_ref = all_stats.as_slice();
self.notify(|obs| obs.on_run_end(termination_cause, all_stats_ref));
Ok(EdaResult {
population: pop,
best,
best_fitness,
generations,
learned_model: best_model,
})
}
fn estimate_bernoulli_ref(selected: &[&U], dim: usize) -> Vec<f64> {
let n = selected.len() as f64;
(0..dim)
.map(|i| {
let ones = selected
.iter()
.filter(|ind| ind.dna().get(i).map(|g| g.id() == 1).unwrap_or(false))
.count() as f64;
(ones / n).clamp(0.01, 0.99)
})
.collect()
}
}
pub struct EdaRealEngine<U: LinearChromosome>
where
U::Gene: RealGene,
{
config: EdaConfiguration,
init_fn: Arc<dyn Fn(usize) -> Vec<U> + Send + Sync>,
fitness_fn: Arc<FitnessFn<U::Gene>>,
observer: Option<Arc<dyn GaObserver<U> + Send + Sync>>,
fitness_cache: Option<Arc<Mutex<crate::fitness::cache::FitnessCache>>>,
}
impl<U: LinearChromosome + Clone> EdaRealEngine<U>
where
U::Gene: RealGene,
{
pub fn new(
config: EdaConfiguration,
init_fn: impl Fn(usize) -> Vec<U> + Send + Sync + 'static,
fitness_fn: impl Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
) -> Self {
Self {
config,
init_fn: Arc::new(init_fn),
fitness_fn: Arc::new(fitness_fn),
observer: None,
fitness_cache: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn GaObserver<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
fn notify<F: FnOnce(&dyn GaObserver<U>)>(&self, f: F) {
if let Some(ref obs) = self.observer {
f(obs.as_ref());
}
}
#[inline]
fn is_better(&self, candidate: f64, current: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => candidate < current,
ProblemSolving::Maximization => candidate > current,
ProblemSolving::FixedFitness => {
if let Some(t) = self.config.fitness_target {
(candidate - t).abs() < (current - t).abs()
} else {
candidate < current
}
}
}
}
fn reached_target(&self, fitness: f64, target: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => fitness <= target,
ProblemSolving::Maximization => fitness >= target,
ProblemSolving::FixedFitness => (fitness - target).abs() < 1e-6,
}
}
fn find_best(&self, pop: &[U]) -> (usize, f64) {
assert!(
!pop.is_empty(),
"EdaRealEngine::find_best called with empty population"
);
let mut best_idx = 0;
let mut best_fit = pop[0].fitness();
for (i, ind) in pop.iter().enumerate().skip(1) {
if self.is_better(ind.fitness(), best_fit) {
best_fit = ind.fitness();
best_idx = i;
}
}
(best_idx, best_fit)
}
fn estimate_gaussian(selected: &[&U], dim: usize) -> (Vec<f64>, Vec<f64>) {
let n = selected.len() as f64;
let mut means = Vec::with_capacity(dim);
let mut stds = Vec::with_capacity(dim);
for i in 0..dim {
let vals: Vec<f64> = selected
.iter()
.filter_map(|ind| ind.dna().get(i).map(|g| g.real_value()))
.collect();
let mean = vals.iter().sum::<f64>() / n;
let variance = if n > 1.0 {
vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0)
} else {
0.0
};
let std = variance.sqrt().max(1e-6);
means.push(mean);
stds.push(std);
}
(means, stds)
}
fn sample_gaussian<R: Rng>(template: &U, means: &[f64], stds: &[f64], rng: &mut R) -> U {
let new_dna: Vec<U::Gene> = template
.dna()
.iter()
.enumerate()
.map(|(i, gene)| {
let mean = means.get(i).copied().unwrap_or(0.0);
let std = stds.get(i).copied().unwrap_or(1.0);
let u1: f64 = rng.random::<f64>().max(1e-10);
let u2: f64 = rng.random::<f64>();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
let mut v = mean + std * z;
if let Some((lo, hi)) = gene.bounds() {
v = v.clamp(lo, hi);
}
gene.with_real_value(v)
})
.collect();
let mut offspring = template.clone();
offspring.set_dna(Cow::Owned(new_dna));
offspring
}
pub fn run(&mut self) -> Result<EdaResult<U>, GaError>
where
U::Gene: Debug,
{
let mut rng = make_rng();
let is_maximization = matches!(self.config.problem_solving, ProblemSolving::Maximization);
if let Some(size) = self.config.fitness_cache_size {
if self.fitness_cache.is_none() {
let (wrapped_fn, cache_handle) =
crate::fitness::cache::wrap_with_cache(Arc::clone(&self.fitness_fn), size);
self.fitness_fn = wrapped_fn;
self.fitness_cache = Some(cache_handle);
}
}
let cmp = |a_fit: f64, b_fit: f64| -> std::cmp::Ordering {
match self.config.problem_solving {
ProblemSolving::Maximization => b_fit
.partial_cmp(&a_fit)
.unwrap_or(std::cmp::Ordering::Equal),
ProblemSolving::Minimization => a_fit
.partial_cmp(&b_fit)
.unwrap_or(std::cmp::Ordering::Equal),
ProblemSolving::FixedFitness => {
let t = self.config.fitness_target.unwrap_or(0.0);
let da = (a_fit - t).abs();
let db = (b_fit - t).abs();
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
}
}
};
self.notify(|obs| obs.on_run_start());
let pop_size = if self.config.population_size == 0 {
100
} else {
self.config.population_size
};
let mut pop: Vec<U> = (self.init_fn)(pop_size.max(1));
if pop.is_empty() {
return Err(GaError::InitializationError(
"EdaRealEngine: init_fn returned an empty population".to_string(),
));
}
for ind in &mut pop {
let f = (self.fitness_fn)(ind.dna());
ind.set_fitness(f);
}
let (best_idx, mut best_fitness) = self.find_best(&pop);
let mut best = pop[best_idx].clone();
let dim = pop[0].dna().len();
self.notify(|obs| obs.on_new_best(0, &best));
let mut termination_cause = TerminationCause::GenerationLimitReached;
let mut all_stats: Vec<GenerationStats> = Vec::with_capacity(self.config.max_generations);
let mut learned_model = EdaModel::Gaussian {
means: vec![0.0; dim],
stds: vec![1.0; dim],
};
let mut best_model = learned_model.clone();
let (mut prev_cache_hits, mut prev_cache_misses) = match &self.fitness_cache {
Some(ch) => {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
(c.hits(), c.misses())
}
None => (0, 0),
};
for gen in 0..self.config.max_generations {
self.notify(|obs| obs.on_generation_start(gen));
let n_selected = ((pop_size as f64 * self.config.selection_ratio).floor() as usize)
.max(1)
.min(pop.len());
let mut indices: Vec<usize> = (0..pop.len()).collect();
indices.select_nth_unstable_by(n_selected - 1, |&a, &b| {
cmp(pop[a].fitness(), pop[b].fitness())
});
let selected: Vec<&U> = indices[..n_selected].iter().map(|&i| &pop[i]).collect();
let (means, stds) = Self::estimate_gaussian(&selected, dim);
learned_model = EdaModel::Gaussian {
means: means.clone(),
stds: stds.clone(),
};
let template = pop[0].clone();
let mut new_pop: Vec<U> = (0..pop_size)
.map(|_| Self::sample_gaussian(&template, &means, &stds, &mut rng))
.collect();
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
{
use rayon::prelude::*;
let fitness_fn = Arc::clone(&self.fitness_fn);
let fitnesses: Vec<f64> = new_pop
.par_iter()
.map(|ind| fitness_fn(ind.dna()))
.collect();
for (ind, f) in new_pop.iter_mut().zip(fitnesses.into_iter()) {
ind.set_fitness(f);
}
}
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
{
for ind in &mut new_pop {
let f = (self.fitness_fn)(ind.dna());
ind.set_fitness(f);
}
}
pop = new_pop;
let (gen_best_idx, gen_best_fit) = self.find_best(&pop);
if self.is_better(gen_best_fit, best_fitness) {
best_fitness = gen_best_fit;
best = pop[gen_best_idx].clone();
best_model = learned_model.clone();
self.notify(|obs| obs.on_new_best(gen, &best));
}
let fitness_values: Vec<f64> = pop.iter().map(|c| c.fitness()).collect();
let mut stats =
GenerationStats::from_fitness_values(gen, &fitness_values, is_maximization);
if let Some(ref ch) = self.fitness_cache {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
stats.cache_hits = Some(c.hits().saturating_sub(prev_cache_hits));
stats.cache_misses = Some(c.misses().saturating_sub(prev_cache_misses));
prev_cache_hits = c.hits();
prev_cache_misses = c.misses();
}
self.notify(|obs| obs.on_generation_end(&stats));
all_stats.push(stats);
if let Some(target) = self.config.fitness_target {
if self.reached_target(best_fitness, target) {
termination_cause = TerminationCause::FitnessTargetReached;
break;
}
}
}
let generations = all_stats.len();
let all_stats_ref = all_stats.as_slice();
self.notify(|obs| obs.on_run_end(termination_cause, all_stats_ref));
Ok(EdaResult {
population: pop,
best,
best_fitness,
generations,
learned_model: best_model,
})
}
}