use std::sync::Arc;
use crate::configuration::{CrossoverConfiguration, ProblemSolving};
use crate::error::GaError;
use crate::operations::crossover;
use crate::operations::mutation::ValueMutable;
use crate::rng::make_rng;
use crate::traits::{FitnessFn, LinearChromosome, MutationOperator, SelectionOperator};
use rand::Rng;
use super::configuration::{CellularConfiguration, Neighborhood, UpdateMode};
pub struct CellularResult<U: LinearChromosome> {
pub population: Vec<U>,
pub best: U,
pub best_fitness: f64,
pub generations: usize,
}
pub struct CellularEngine<U: LinearChromosome> {
config: CellularConfiguration,
init_fn: Arc<dyn Fn(usize) -> Vec<U> + Send + Sync>,
fitness_fn: Arc<FitnessFn<U::Gene>>,
}
impl<U> CellularEngine<U>
where
U: LinearChromosome,
{
pub fn new(
config: CellularConfiguration,
init_fn: impl Fn(usize) -> Vec<U> + Send + Sync + 'static,
fitness_fn: impl Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
) -> Result<Self, GaError> {
if config.rows == 0 || config.cols == 0 {
return Err(GaError::ConfigurationError(
"CellularEngine: rows and cols must both be > 0".to_string(),
));
}
Ok(Self {
config,
init_fn: Arc::new(init_fn),
fitness_fn: Arc::new(fitness_fn),
})
}
}
impl<U> CellularEngine<U>
where
U: LinearChromosome + Clone + ValueMutable + crate::traits::RealValuedMutation + 'static,
{
pub fn run(&mut self) -> CellularResult<U> {
let rows = self.config.rows;
let cols = self.config.cols;
let pop_size = rows * cols;
let mut pop: Vec<U> = (self.init_fn)(pop_size);
for ind in &mut pop {
let f = (self.fitness_fn)(ind.dna());
ind.set_fitness(f);
}
debug_assert!(!pop.is_empty(), "grid validated > 0 in new()");
let mut best_fitness = pop[0].fitness();
let mut best = pop[0].clone();
for ind in &pop {
if self.is_better(ind.fitness(), best_fitness) {
best_fitness = ind.fitness();
best = ind.clone();
}
}
let crossover_cfg = CrossoverConfiguration {
method: self.config.crossover,
..CrossoverConfiguration::default()
};
let mut rng = make_rng();
let mut generations = 0usize;
for _gen in 0..self.config.max_generations {
let source: Vec<U> = match self.config.update_mode {
UpdateMode::Synchronous => pop.clone(), UpdateMode::Asynchronous => vec![], };
let is_sync = matches!(self.config.update_mode, UpdateMode::Synchronous);
let mut replacements: Vec<(usize, U)> = Vec::new();
for row in 0..rows {
for col in 0..cols {
let cell_idx = row * cols + col;
let neighbor_idxs = self.neighbors(row, col, rows, cols);
if neighbor_idxs.is_empty() {
continue;
}
let src_pop: &[U] = if is_sync { &source } else { &pop };
let mut local: Vec<U> = Vec::with_capacity(neighbor_idxs.len() + 1);
local.push(src_pop[cell_idx].clone()); for &ni in &neighbor_idxs {
local.push(src_pop[ni].clone());
}
let pairs = match self.config.selection.select(&local, 1, 1, 2) {
Ok(p) => p,
Err(_) => {
crate::log_warn!(target: "selection_events",
"CellularEngine: configured selection cannot run through the \
trait (e.g. Lexicase); skipping mate selection for this cell");
vec![vec![0, 1]]
}
};
let mate_local_idx = if let Some(group) = pairs.first() {
let a = group[0];
let b = group[1];
if a != 0 {
a
} else if b != 0 {
b
} else {
rng.random_range(1..local.len())
}
} else {
rng.random_range(1..local.len())
};
let parent_cell = &src_pop[cell_idx];
let parent_mate = &local[mate_local_idx];
let mut offspring =
match crossover::factory(parent_cell, parent_mate, crossover_cfg) {
Ok(children) if !children.is_empty() => {
children.into_iter().next().unwrap()
}
_ => parent_cell.clone(),
};
let _ = self
.config
.mutation
.mutate(&mut offspring, &self.config.mutation);
let offspring_fitness = (self.fitness_fn)(offspring.dna());
offspring.set_fitness(offspring_fitness);
if self.is_better(offspring_fitness, src_pop[cell_idx].fitness()) {
if self.is_better(offspring_fitness, best_fitness) {
best_fitness = offspring_fitness;
best = offspring.clone();
}
if is_sync {
replacements.push((cell_idx, offspring));
} else {
pop[cell_idx] = offspring;
}
}
}
}
if is_sync {
for (idx, ind) in replacements {
pop[idx] = ind;
}
}
generations += 1;
if let Some(target) = self.config.fitness_target {
if self.reached_target(best_fitness, target) {
break;
}
}
}
CellularResult {
population: pop,
best,
best_fitness,
generations,
}
}
fn neighbors(&self, row: usize, col: usize, rows: usize, cols: usize) -> Vec<usize> {
match &self.config.neighborhood {
Neighborhood::VonNeumann => {
let mut v = Vec::with_capacity(4);
let r = row;
let c = col;
v.push(((r + rows - 1) % rows) * cols + c);
v.push(((r + 1) % rows) * cols + c);
v.push(r * cols + (c + cols - 1) % cols);
v.push(r * cols + (c + 1) % cols);
v.sort_unstable();
v.dedup();
v.retain(|&i| i != row * cols + col);
v
}
Neighborhood::Moore => {
let mut v = Vec::with_capacity(8);
for dr in [-1i64, 0, 1] {
for dc in [-1i64, 0, 1] {
if dr == 0 && dc == 0 {
continue;
}
let nr = ((row as i64 + dr).rem_euclid(rows as i64)) as usize;
let nc = ((col as i64 + dc).rem_euclid(cols as i64)) as usize;
v.push(nr * cols + nc);
}
}
v.sort_unstable();
v.dedup();
v
}
Neighborhood::CompactR2 => {
let mut v = Vec::with_capacity(24);
for dr in [-2i64, -1, 0, 1, 2] {
for dc in [-2i64, -1, 0, 1, 2] {
if dr == 0 && dc == 0 {
continue;
}
let nr = ((row as i64 + dr).rem_euclid(rows as i64)) as usize;
let nc = ((col as i64 + dc).rem_euclid(cols as i64)) as usize;
v.push(nr * cols + nc);
}
}
v.sort_unstable();
v.dedup();
v.retain(|&i| i != row * cols + col);
v
}
Neighborhood::Linear => {
let idx = row * cols + col;
let n = rows * cols;
let left = (idx + n - 1) % n;
let right = (idx + 1) % n;
let mut v = vec![left, right];
v.sort_unstable();
v.dedup();
v.retain(|&i| i != idx);
v
}
}
}
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,
}
}
}