pub(crate) mod adaptive;
pub(crate) mod aos;
pub(crate) mod batch;
pub(crate) mod cache;
pub(crate) mod extension;
pub(crate) mod generation;
pub(crate) mod lifecycle;
pub(crate) mod observer;
pub(crate) mod stats;
pub(crate) mod stopping;
use crate::aos::AosState;
use crate::configuration::GaConfiguration;
use crate::constraints::{ConstraintHandling, PenaltyStrategy};
use crate::error::GaError;
use crate::hall_of_fame::{HallOfFame, HallOfFameConfig};
use crate::observer::{ExtensionEvent, GaObserver};
use crate::stats::GenerationStats;
use crate::traits::{FitnessFn, InitializationFn, MutationOperator};
use crate::validators::validator_factory as ValidatorFactory;
use crate::{
configuration::{LimitConfiguration, LocalSearchConfiguration, ProblemSolving},
operations::local_search::{LocalSearch, LocalSearchApplicationStrategy, LocalSearchMode},
operations::{
crossover, extension as extension_ops, mutation, selection, survivor, Crossover, Extension,
Mutation,
},
population::Population,
traits::{
ConfigurationT, CrossoverConfig, ElitismConfig, ExtensionConfig, GeneT, LinearChromosome,
LocalSearchConfig, LocalSearchOperator, MutationConfig, NichingConfig, OperatorCompat,
SelectionConfig, StoppingConfig, Strategy, SurvivorConfig, VectorFitness,
},
};
use rand::Rng;
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
use rayon::prelude::*;
use std::fmt::Debug;
use std::ops::ControlFlow;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
type ConstraintFn<G> = Arc<dyn Fn(&[G]) -> f64 + Send + Sync>;
type RepairFn<U> = Arc<dyn Fn(&mut U) -> Result<(), GaError> + Send + Sync>;
type RewardAccumulator = Option<Arc<Mutex<Vec<(usize, f64)>>>>;
#[cfg(feature = "serde")]
pub trait MaybeSerialize: serde::Serialize {}
#[cfg(feature = "serde")]
impl<T: serde::Serialize> MaybeSerialize for T {}
#[cfg(not(feature = "serde"))]
pub trait MaybeSerialize {}
#[cfg(not(feature = "serde"))]
impl<T> MaybeSerialize for T {}
#[cfg(not(feature = "serde"))]
pub trait MaybeDeserialize {}
#[cfg(not(feature = "serde"))]
impl<T> MaybeDeserialize for T {}
#[cfg(feature = "serde")]
pub trait MaybeDeserialize: for<'de> serde::Deserialize<'de> {}
#[cfg(feature = "serde")]
impl<T: for<'de> serde::Deserialize<'de>> MaybeDeserialize for T {}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TerminationCause {
GenerationLimitReached,
FitnessTargetReached,
StagnationReached,
ConvergenceReached,
TimeLimitReached,
CallbackRequested,
NotTerminated,
}
pub struct Ga<U>
where
U: LinearChromosome,
{
pub configuration: GaConfiguration,
pub alleles: Vec<U::Gene>,
pub population: Population<U>,
pub termination_cause: TerminationCause,
pub initialization_fn: Option<Arc<InitializationFn<U::Gene>>>,
pub fitness_fn: Option<Arc<FitnessFn<U::Gene>>>,
stats: Vec<GenerationStats>,
dynamic_mutation_probability: f64,
fitness_cache_size: Option<usize>,
fitness_cache: Option<std::sync::Arc<std::sync::Mutex<crate::fitness::cache::FitnessCache>>>,
batch_evaluator: Option<Arc<dyn crate::fitness::BatchFitnessEvaluator<U> + Send + Sync>>,
surrogate: Option<(
Arc<dyn crate::fitness::SurrogateModel<U> + Send + Sync>,
f64,
)>,
observer: Option<Arc<dyn GaObserver<U> + Send + Sync>>,
constraint_fns: Option<Vec<ConstraintFn<U::Gene>>>,
penalty_strategy: PenaltyStrategy,
constraint_handling: Option<ConstraintHandling>,
repair_operator: Option<RepairFn<U>>,
penalty_coefficient: f64,
adaptive_penalty_counter: isize,
hall_of_fame: Option<HallOfFame<U>>,
seeds: Option<Vec<U>>,
checkpoint_path: Option<PathBuf>,
aos_crossover: Option<Mutex<AosState>>,
aos_mutation: Option<Mutex<AosState>>,
}
impl<U> Ga<U>
where
U: LinearChromosome,
{
pub fn configuration(&self) -> &GaConfiguration {
&self.configuration
}
}
impl<U> Default for Ga<U>
where
U: LinearChromosome,
{
fn default() -> Self {
Ga {
configuration: GaConfiguration {
..Default::default()
},
population: Population::new_empty(),
alleles: Vec::new(),
termination_cause: TerminationCause::NotTerminated,
initialization_fn: None,
fitness_fn: None,
stats: Vec::new(),
dynamic_mutation_probability: 1.0,
fitness_cache_size: None,
fitness_cache: None,
batch_evaluator: None,
surrogate: None,
observer: None,
constraint_fns: None,
penalty_strategy: PenaltyStrategy::None,
constraint_handling: None,
repair_operator: None,
penalty_coefficient: 0.0,
adaptive_penalty_counter: 0,
hall_of_fame: None,
seeds: None,
checkpoint_path: None,
aos_crossover: None,
aos_mutation: None,
}
}
}
impl<U> SelectionConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_number_of_couples(mut self, number_of_couples: usize) -> Self {
self.configuration.selection_configuration.number_of_couples = number_of_couples;
self
}
fn with_selection_method(mut self, selection_method: crate::operations::Selection) -> Self {
self.configuration.selection_configuration.method = selection_method;
self
}
fn with_niche_radius(mut self, niche_radius: f64) -> Self {
self.configuration.selection_configuration.niche_radius = niche_radius;
self
}
fn with_epsilon_lexicase(mut self, epsilon: f64) -> Self {
self.configuration.selection_configuration.epsilon = epsilon;
self
}
}
impl<U> CrossoverConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_crossover_number_of_points(mut self, number_of_points: usize) -> Self {
self.configuration.crossover_configuration.number_of_points = Some(number_of_points);
self
}
fn with_crossover_probability_max(mut self, probability_max: f64) -> Self {
self.configuration.crossover_configuration.probability_max = Some(probability_max);
self
}
fn with_crossover_probability_min(mut self, probability_min: f64) -> Self {
self.configuration.crossover_configuration.probability_min = Some(probability_min);
self
}
fn with_crossover_method(mut self, method: crossover::Crossover) -> Self {
self.configuration.crossover_configuration.method = method;
self
}
fn with_sbx_eta(mut self, eta: f64) -> Self {
self.configuration.crossover_configuration.sbx_eta = Some(eta);
self
}
fn with_blend_alpha(mut self, alpha: f64) -> Self {
self.configuration.crossover_configuration.blend_alpha = Some(alpha);
self
}
fn with_undx_sigma_xi(mut self, value: f64) -> Self {
self.configuration.crossover_configuration.undx_sigma_xi = Some(value);
self
}
fn with_undx_sigma_eta(mut self, value: f64) -> Self {
self.configuration.crossover_configuration.undx_sigma_eta = Some(value);
self
}
fn with_pcx_sigma_eta(mut self, value: f64) -> Self {
self.configuration.crossover_configuration.pcx_sigma_eta = Some(value);
self
}
fn with_pcx_sigma_zeta(mut self, value: f64) -> Self {
self.configuration.crossover_configuration.pcx_sigma_zeta = Some(value);
self
}
}
impl<U> MutationConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_mutation_probability_max(mut self, probability_max: f64) -> Self {
self.configuration.mutation_configuration.probability_max = Some(probability_max);
self
}
fn with_mutation_probability_min(mut self, probability_min: f64) -> Self {
self.configuration.mutation_configuration.probability_min = Some(probability_min);
self
}
fn with_mutation_method(mut self, method: crate::operations::Mutation) -> Self {
self.configuration.mutation_configuration.method = method;
self
}
fn with_dynamic_mutation(mut self, enabled: bool) -> Self {
self.configuration.mutation_configuration.dynamic_mutation = enabled;
self
}
fn with_mutation_target_cardinality(mut self, target: f64) -> Self {
self.configuration.mutation_configuration.target_cardinality = Some(target);
self
}
fn with_mutation_probability_step(mut self, step: f64) -> Self {
self.configuration.mutation_configuration.probability_step = Some(step);
self
}
}
impl<U> StoppingConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_max_generations(mut self, max_generations: usize) -> Self {
self.configuration.limit_configuration.max_generations = max_generations;
self
}
fn with_fitness_target(mut self, fitness_target: f64) -> Self {
self.configuration.limit_configuration.fitness_target = Some(fitness_target);
self
}
fn with_stagnation_limit(mut self, n: usize) -> Self {
self.configuration.stagnation_generations = Some(n);
self
}
fn with_convergence_threshold(mut self, threshold: f64) -> Self {
self.configuration.convergence_threshold = Some(threshold);
self
}
fn with_max_duration_secs(mut self, secs: f64) -> Self {
self.configuration.max_duration_secs = Some(secs);
self
}
}
impl<U> NichingConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_niching_enabled(mut self, enabled: bool) -> Self {
self.configuration
.niching_configuration
.get_or_insert_with(crate::niching::configuration::NichingConfiguration::default)
.enabled = enabled;
self
}
fn with_niching_sigma_share(mut self, sigma_share: f64) -> Self {
self.configuration
.niching_configuration
.get_or_insert_with(crate::niching::configuration::NichingConfiguration::default)
.sigma_share = sigma_share;
self
}
fn with_niching_alpha(mut self, alpha: f64) -> Self {
self.configuration
.niching_configuration
.get_or_insert_with(crate::niching::configuration::NichingConfiguration::default)
.alpha = alpha;
self
}
}
impl<U> ElitismConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_elitism(mut self, elitism_count: usize) -> Self {
self.configuration.elitism_count = elitism_count;
self
}
}
impl<U> SurvivorConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_length_penalty(mut self, penalty: f64) -> Self {
self.configuration.length_penalty = Some(penalty);
self
}
}
impl<U> ExtensionConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_extension_method(mut self, method: crate::operations::Extension) -> Self {
self.configuration
.extension_configuration
.get_or_insert_with(crate::extension::configuration::ExtensionConfiguration::default)
.method = method;
self
}
fn with_extension_diversity_threshold(mut self, threshold: f64) -> Self {
self.configuration
.extension_configuration
.get_or_insert_with(crate::extension::configuration::ExtensionConfiguration::default)
.diversity_threshold = threshold;
self
}
fn with_extension_survival_rate(mut self, rate: f64) -> Self {
self.configuration
.extension_configuration
.get_or_insert_with(crate::extension::configuration::ExtensionConfiguration::default)
.survival_rate = rate;
self
}
fn with_extension_mutation_rounds(mut self, rounds: usize) -> Self {
self.configuration
.extension_configuration
.get_or_insert_with(crate::extension::configuration::ExtensionConfiguration::default)
.mutation_rounds = rounds;
self
}
fn with_extension_elite_count(mut self, count: usize) -> Self {
self.configuration
.extension_configuration
.get_or_insert_with(crate::extension::configuration::ExtensionConfiguration::default)
.elite_count = count;
self
}
}
impl<U> LocalSearchConfig for Ga<U>
where
U: LinearChromosome,
{
fn with_local_search_configuration(mut self, config: LocalSearchConfiguration) -> Self {
self.configuration.local_search_configuration = Some(config);
self
}
}
impl<U> ConfigurationT for Ga<U>
where
U: LinearChromosome,
{
fn new() -> Self {
Self::default()
}
fn with_adaptive_ga(mut self, adaptive_ga: bool) -> Self {
self.configuration.adaptive_ga = adaptive_ga;
self
}
fn with_threads(mut self, number_of_threads: usize) -> Self {
self.configuration.number_of_threads = number_of_threads;
self
}
fn with_survivor_method(mut self, method: crate::operations::Survivor) -> Self {
self.configuration.survivor = method;
self
}
fn with_problem_solving(mut self, problem_solving: ProblemSolving) -> Self {
self.configuration.limit_configuration.problem_solving = problem_solving;
self
}
fn with_population_size(mut self, population_size: usize) -> Self {
self.configuration.limit_configuration.population_size = population_size;
self.configuration.selection_configuration.number_of_couples =
if self.configuration.selection_configuration.number_of_couples == 0 {
self.configuration.limit_configuration.population_size / 2
} else {
self.configuration.selection_configuration.number_of_couples
};
self
}
fn with_chromosome_length(mut self, length: crate::chromosomes::ChromosomeLength) -> Self {
self.configuration.limit_configuration.chromosome_length = length;
self
}
fn with_save_progress(mut self, save_progress: bool) -> Self {
self.configuration.save_progress_configuration.save_progress = save_progress;
self
}
fn with_save_progress_interval(mut self, save_progress_interval: usize) -> Self {
self.configuration
.save_progress_configuration
.save_progress_interval = save_progress_interval;
self
}
fn with_save_progress_path(mut self, save_progress_path: String) -> Self {
self.configuration
.save_progress_configuration
.save_progress_path = save_progress_path;
self
}
fn with_rng_seed(mut self, seed: u64) -> Self {
self.configuration.rng_seed = Some(seed);
self
}
fn with_crossover_portfolio(mut self, portfolio: Vec<crate::operations::Crossover>) -> Self {
self.configuration.crossover_portfolio = Some(portfolio);
self
}
fn with_mutation_portfolio(mut self, portfolio: Vec<crate::operations::Mutation>) -> Self {
self.configuration.mutation_portfolio = Some(portfolio);
self
}
fn with_aos_strategy(mut self, strategy: crate::aos::AosStrategy) -> Self {
self.configuration.aos_strategy = strategy;
self
}
fn with_reward_window(mut self, window: usize) -> Self {
self.configuration.aos_reward_window = window;
self
}
}
impl<U> Ga<U>
where
U: LinearChromosome
+ Send
+ Sync
+ 'static
+ Clone
+ Debug
+ mutation::ValueMutable
+ MaybeSerialize
+ MaybeDeserialize
+ OperatorCompat
+ crate::traits::RealValuedMutation,
U::Gene: 'static + Debug,
{
pub fn build(mut self) -> Result<Self, GaError> {
if self.configuration.selection_configuration.number_of_couples == 0
&& self.configuration.limit_configuration.population_size > 0
{
self.configuration.selection_configuration.number_of_couples =
self.configuration.limit_configuration.population_size / 2;
}
ValidatorFactory::validate::<U>(
Some(&self.configuration),
None,
if self.alleles.is_empty() {
None
} else {
Some(&self.alleles)
},
)?;
crate::validators::generic_validator::operator_compat_check::<U>(&self.configuration)?;
match self.configuration.crossover_configuration.method {
crate::operations::Crossover::Undx { num_parents }
| crate::operations::Crossover::Spx { num_parents }
| crate::operations::Crossover::Pcx { num_parents }
if num_parents < 3 =>
{
return Err(GaError::ConfigurationError(format!(
"Multi-parent crossover requires num_parents >= 3, got {}",
num_parents
)));
}
_ => {}
}
if self.fitness_fn.is_some() && self.batch_evaluator.is_some() {
return Err(GaError::ConfigurationError(
"Cannot use both fitness_fn and with_batch_evaluator() — they are mutually exclusive"
.to_string(),
));
}
if let Some((_, fraction)) = &self.surrogate {
if *fraction <= 0.0 || *fraction > 1.0 {
return Err(GaError::ConfigurationError(
"prescreening_fraction must be in (0.0, 1.0]".to_string(),
));
}
}
if let Some(cache_size) = self.fitness_cache_size {
if let Some(fitness_fn) = self.fitness_fn.take() {
let (wrapped, cache_handle) =
crate::fitness::cache::wrap_with_cache(fitness_fn, cache_size);
self.fitness_fn = Some(wrapped);
self.fitness_cache = Some(cache_handle);
}
}
crate::constraints::validate_penalty_strategy(&self.penalty_strategy)?;
if self.seeds.is_some() && self.checkpoint_path.is_some() {
return Err(GaError::ConfigurationError(
"Cannot use both with_seeds() and with_checkpoint() — they are mutually exclusive"
.to_string(),
));
}
if let Some(ref seeds) = self.seeds {
let pop_size = self.configuration.limit_configuration.population_size;
if seeds.len() > pop_size {
return Err(GaError::ConfigurationError(format!(
"Number of seeds ({}) exceeds population_size ({}): with_seeds count must not exceed population_size",
seeds.len(),
pop_size,
)));
}
}
if let Some(ref checkpoint_path) = self.checkpoint_path {
if !checkpoint_path.exists() {
return Err(GaError::CheckpointError(format!(
"Checkpoint file not found: {}",
checkpoint_path.display(),
)));
}
}
if let Some(ref xover_pf) = self.configuration.crossover_portfolio {
if xover_pf.is_empty() {
return Err(GaError::ConfigurationError(
"AOS crossover portfolio is empty: provide at least 2 operators".to_string(),
));
}
if xover_pf.len() == 1 {
crate::log_warn!(target: "ga_events", "AOS crossover portfolio has only 1 operator; portfolio mode is effectively the same as single-operator mode");
}
}
if let Some(ref mut_pf) = self.configuration.mutation_portfolio {
if mut_pf.is_empty() {
return Err(GaError::ConfigurationError(
"AOS mutation portfolio is empty: provide at least 2 operators".to_string(),
));
}
if mut_pf.len() == 1 {
crate::log_warn!(target: "ga_events", "AOS mutation portfolio has only 1 operator; portfolio mode is effectively the same as single-operator mode");
}
}
if self.configuration.crossover_portfolio.is_some()
&& self.configuration.crossover_configuration.method
!= crate::operations::Crossover::Uniform
{
crate::log_warn!(target: "ga_events", "Both crossover portfolio and with_crossover_method() are configured. with_crossover_method() will be ignored when portfolio is set");
}
if self.configuration.mutation_portfolio.is_some()
&& self.configuration.mutation_configuration.method != crate::operations::Mutation::Swap
{
crate::log_warn!(target: "ga_events", "Both mutation portfolio and with_mutation_method() are configured. with_mutation_method() will be ignored when portfolio is set");
}
Ok(self)
}
pub fn with_alleles(mut self, alleles: Vec<U::Gene>) -> Self {
self.alleles = alleles;
self
}
pub fn with_population(mut self, population: Population<U>) -> Self {
if self.configuration.selection_configuration.number_of_couples == 0 {
self.configuration.selection_configuration.number_of_couples = population.size() / 2;
}
self.population = population;
self
}
pub fn with_fitness_fn<F>(mut self, fitness_fn: F) -> Self
where
F: Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
{
self.fitness_fn = Some(Arc::new(fitness_fn));
self
}
pub fn with_observer(mut self, observer: Arc<dyn GaObserver<U> + Send + Sync>) -> Self {
self.observer = Some(observer);
self
}
#[inline]
fn notify<F: FnOnce(&dyn GaObserver<U>)>(&self, f: F) {
observer::dispatch(&self.observer, f);
}
pub fn with_fitness_cache_size(mut self, size: usize) -> Self {
self.fitness_cache_size = Some(size);
self
}
pub fn with_batch_evaluator(
mut self,
evaluator: Arc<dyn crate::fitness::BatchFitnessEvaluator<U> + Send + Sync>,
) -> Self {
self.batch_evaluator = Some(evaluator);
self
}
pub fn with_surrogate(
mut self,
model: Arc<dyn crate::fitness::SurrogateModel<U> + Send + Sync>,
prescreening_fraction: f64,
) -> Self {
self.surrogate = Some((model, prescreening_fraction));
self
}
pub fn with_constraint_fns<F>(mut self, fns: Vec<F>) -> Self
where
F: Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
{
self.constraint_fns = Some(fns.into_iter().map(|f| Arc::new(f) as Arc<_>).collect());
self
}
pub fn with_penalty_strategy(mut self, strategy: PenaltyStrategy) -> Self {
self.penalty_strategy = strategy;
self
}
pub fn with_constraint_handling(mut self, handling: ConstraintHandling) -> Self {
self.constraint_handling = Some(handling);
self
}
pub fn with_repair_operator<F>(mut self, operator: F) -> Self
where
F: Fn(&mut U) -> Result<(), GaError> + Send + Sync + 'static,
{
self.repair_operator = Some(Arc::new(operator));
self
}
pub fn with_initialization_fn<F>(mut self, initialization_fn: F) -> Self
where
U: LinearChromosome + Send + Sync + 'static + Clone,
F: Fn(usize, Option<&[U::Gene]>) -> Vec<U::Gene> + Send + Sync + 'static,
{
self.initialization_fn = Some(Arc::new(initialization_fn));
self
}
pub fn with_hall_of_fame(mut self, config: HallOfFameConfig) -> Self {
self.hall_of_fame = Some(HallOfFame::new(config));
self
}
pub fn with_seeds(mut self, seeds: Vec<U>) -> Self {
self.seeds = Some(seeds);
self
}
pub fn with_checkpoint(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.checkpoint_path = Some(path.into());
self
}
pub fn with_local_search(mut self, method: LocalSearch) -> Self {
self.configuration
.local_search_configuration
.get_or_insert_with(LocalSearchConfiguration::default)
.method = method;
self
}
pub fn run(&mut self) -> Result<&Population<U>, GaError> {
self.run_with_callback(
None::<
fn(&usize, &Population<U>, &GenerationStats, &TerminationCause) -> ControlFlow<()>,
>,
0,
)
}
pub fn run_with_callback<F>(
&mut self,
callback: Option<F>,
generations_to_callback: usize,
) -> Result<&Population<U>, GaError>
where
U: LinearChromosome + Send + Sync + 'static + Clone + MaybeDeserialize,
F: Fn(&usize, &Population<U>, &GenerationStats, &TerminationCause) -> ControlFlow<()>,
{
ValidatorFactory::validate::<U>(Some(&self.configuration), None, Some(&self.alleles))?;
if matches!(
self.configuration.selection_configuration.method,
crate::operations::Selection::Lexicase | crate::operations::Selection::EpsilonLexicase
) {
return Err(GaError::ConfigurationError(
"Lexicase / EpsilonLexicase selection requires a chromosome implementing \
VectorFitness; use run_lexicase() or run_lexicase_with_callback() instead of \
run() / run_with_callback()."
.to_string(),
));
}
crate::rng::set_seed(self.configuration.rng_seed);
#[cfg_attr(not(feature = "serde"), allow(unused_mut))]
let mut checkpoint_generation: Option<usize> = None;
if self.checkpoint_path.is_some() {
#[cfg(feature = "serde")]
{
let path = self.checkpoint_path.take().unwrap();
let ckpt = crate::checkpoint::load_checkpoint::<U>(&path).map_err(|e| {
GaError::CheckpointError(format!(
"Failed to load checkpoint '{}': {}",
path.display(),
e
))
})?;
let builder_selection = self.configuration.selection_configuration.method;
let builder_crossover = self.configuration.crossover_configuration.method;
let builder_mutation = self.configuration.mutation_configuration.method;
let builder_survivor = self.configuration.survivor;
let builder_problem_solving =
self.configuration.limit_configuration.problem_solving;
let builder_max_generations =
self.configuration.limit_configuration.max_generations;
let builder_population_size =
self.configuration.limit_configuration.population_size;
self.configuration = ckpt.configuration;
self.configuration.selection_configuration.method = builder_selection;
self.configuration.crossover_configuration.method = builder_crossover;
self.configuration.mutation_configuration.method = builder_mutation;
self.configuration.survivor = builder_survivor;
self.configuration.limit_configuration.problem_solving = builder_problem_solving;
self.configuration.limit_configuration.max_generations = builder_max_generations;
self.configuration.limit_configuration.population_size = builder_population_size;
self.population = ckpt.population;
self.stats = ckpt.stats; checkpoint_generation = Some(ckpt.generation);
}
#[cfg(not(feature = "serde"))]
{
return Err(GaError::CheckpointError(
"Checkpoint loading requires the 'serde' feature. Enable it in Cargo.toml: genetic_algorithms = { features = [\"serde\"] }".to_string(),
));
}
} else if self.population.size() == 0 && self.initialization_fn.is_some() {
self.initialization()?;
if let Some(eval) = self.batch_evaluator.as_ref().map(Arc::clone) {
let cache = self.fitness_cache.as_ref().map(Arc::clone);
batch::batch_evaluate(eval, cache, &mut self.population.chromosomes)?;
}
} else if self.population.size() == 0 && self.initialization_fn.is_none() {
return Err(GaError::InitializationError(
"No initialization function set".to_string(),
));
}
if self.configuration.adaptive_ga {
self.population.recalculate_aga();
}
aos::init_aos_state(
&self.configuration,
&mut self.aos_crossover,
&mut self.aos_mutation,
);
if self.configuration.mutation_configuration.dynamic_mutation {
self.dynamic_mutation_probability = self
.configuration
.mutation_configuration
.probability_max
.unwrap_or(1.0);
}
if self.batch_evaluator.is_some() && self.fitness_cache.is_none() {
if let Some(size) = self.fitness_cache_size {
self.fitness_cache = Some(Arc::new(Mutex::new(
crate::fitness::cache::FitnessCache::new(size),
)));
}
}
let initial_population_size = self.population.size();
let mut age = 0usize;
self.population.fitness_calculation(
self.configuration.number_of_threads,
self.configuration.limit_configuration.problem_solving,
);
if self.constraint_fns.is_some() {
self.process_constraints_population(0)?;
}
let mut generation_callback_count = 0usize;
if checkpoint_generation.is_none() {
self.stats.clear();
}
let is_maximization = matches!(
self.configuration.limit_configuration.problem_solving,
ProblemSolving::Maximization
);
#[cfg(not(target_arch = "wasm32"))]
let start_time = Instant::now();
#[cfg(target_arch = "wasm32")]
if self.configuration.max_duration_secs.is_some() {
crate::log_warn!(target: "ga_events", "max_duration_secs is not supported on wasm32 — time limit will be ignored");
}
let mut best_fitness_so_far = self.population.best_chromosome.fitness();
let mut stagnation_count: usize = 0;
self.notify(|obs| obs.on_run_start());
let start_gen = checkpoint_generation.unwrap_or(0);
let total_gens = if checkpoint_generation.is_some() {
start_gen + self.configuration.limit_configuration.max_generations
} else {
self.configuration.limit_configuration.max_generations
};
let mut offspring_buf: Vec<U> =
Vec::with_capacity(self.configuration.limit_configuration.population_size * 2);
for i in start_gen..total_gens {
age += 1;
let (prev_cache_hits, prev_cache_misses) = cache::cache_snapshot(&self.fitness_cache)?;
self.notify(|obs| obs.on_generation_start(i));
let t_sel: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let num_parents = match self.configuration.crossover_configuration.method {
crate::operations::Crossover::Undx { num_parents }
| crate::operations::Crossover::Spx { num_parents }
| crate::operations::Crossover::Pcx { num_parents } => num_parents,
_ => 2,
};
let parents = selection::factory(
&self.population.chromosomes,
self.configuration.selection_configuration,
self.configuration.number_of_threads,
num_parents,
)?;
if let Some(t) = t_sel {
self.notify(|obs| obs.on_selection_complete(i, t.elapsed(), parents.len()));
}
let dynamic_prob = if self.configuration.mutation_configuration.dynamic_mutation {
Some(self.dynamic_mutation_probability)
} else {
None
};
let t_cx: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let crossover_fitness_fn = if self.batch_evaluator.is_some() {
None
} else {
self.fitness_fn.clone()
};
generation::parent_crossover(
&parents,
&self.population.chromosomes,
&self.configuration,
generation::ParentCrossoverParams {
age,
f_max: self.population.f_max,
f_avg: self.population.f_avg,
dynamic_mutation_prob: dynamic_prob,
generation: i,
best_fitness: best_fitness_so_far,
is_maximization,
fitness_fn: crossover_fitness_fn,
crossover_portfolio: self.configuration.crossover_portfolio.as_ref(),
mutation_portfolio: self.configuration.mutation_portfolio.as_ref(),
aos_crossover_state: self.aos_crossover.as_ref(),
aos_mutation_state: self.aos_mutation.as_ref(),
},
&mut offspring_buf,
)?;
let true_fitness_calls: Option<u64> = if let Some((ref surrogate, fraction)) =
self.surrogate
{
if offspring_buf.is_empty() {
Some(0)
} else {
let mut scores: Vec<(usize, f64)> = offspring_buf
.iter()
.enumerate()
.map(|(idx, c)| {
let raw = surrogate.predict(c);
let score = if raw.is_nan() { f64::NEG_INFINITY } else { raw };
(idx, score)
})
.collect();
scores.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let keep = ((offspring_buf.len() as f64 * fraction).floor() as usize).max(1);
scores.truncate(keep);
scores.sort_unstable_by_key(|&(idx, _)| idx);
offspring_buf = scores
.into_iter()
.map(|(idx, _)| offspring_buf[idx].clone())
.collect();
Some(offspring_buf.len() as u64)
}
} else {
None
};
if let Some(eval) = self.batch_evaluator.as_ref().map(Arc::clone) {
let cache = self.fitness_cache.as_ref().map(Arc::clone);
batch::batch_evaluate(eval, cache, &mut offspring_buf)?;
}
if let Some(t) = t_cx {
let elapsed = t.elapsed();
let offspring_count = offspring_buf.len();
let pop_size = self.population.chromosomes.len();
self.notify(|obs| obs.on_crossover_complete(i, elapsed, offspring_count));
self.notify(|obs| obs.on_mutation_complete(i, elapsed, pop_size));
self.notify(|obs| obs.on_fitness_evaluation_complete(i, elapsed, pop_size));
}
if let Some(ref repair_op) = self.repair_operator {
for c in offspring_buf.iter_mut() {
repair_op(c)?;
c.calculate_fitness();
}
}
if let Some(ref constraint_fns) = self.constraint_fns {
for c in offspring_buf.iter_mut() {
let dna = c.dna();
let total_viol: f64 = constraint_fns.iter().map(|f| f(dna)).sum();
if total_viol > 0.0 {
match self.penalty_strategy {
PenaltyStrategy::None => {}
PenaltyStrategy::Static { coefficient } => {
c.set_fitness(c.fitness() + coefficient * total_viol);
}
PenaltyStrategy::Dynamic { c: dc, alpha, beta } => {
let penalized = crate::constraints::apply_dynamic_penalty(
c.fitness(),
total_viol,
age,
dc,
alpha,
beta,
);
c.set_fitness(penalized);
}
PenaltyStrategy::Adaptive {
initial_coefficient,
..
} => {
let coeff = if self.penalty_coefficient == 0.0 {
initial_coefficient
} else {
self.penalty_coefficient
};
c.set_fitness(c.fitness() + coeff * total_viol);
}
}
}
}
}
if let Some(ref ls_config) = self.configuration.local_search_configuration {
let strategy = ls_config.application_strategy;
let mode = ls_config.mode;
let should_apply = match strategy {
LocalSearchApplicationStrategy::EveryNGenerations { interval } => {
interval == 0 || (i + 1) % interval == 0
}
_ => true,
};
if should_apply && !offspring_buf.is_empty() {
let candidates: Vec<usize> = match strategy {
LocalSearchApplicationStrategy::AllOffspring => {
(0..offspring_buf.len()).collect()
}
LocalSearchApplicationStrategy::BestN { n } => {
let mut indices: Vec<usize> = (0..offspring_buf.len()).collect();
let ps = self.configuration.limit_configuration.problem_solving;
let k = n.min(indices.len());
if k > 0 {
indices.select_nth_unstable_by(k.saturating_sub(1), |&a, &b| {
let (fa, fb) =
(offspring_buf[a].fitness(), offspring_buf[b].fitness());
match ps {
ProblemSolving::Minimization
| ProblemSolving::FixedFitness => {
fa.partial_cmp(&fb).unwrap_or(std::cmp::Ordering::Equal)
}
ProblemSolving::Maximization => {
fb.partial_cmp(&fa).unwrap_or(std::cmp::Ordering::Equal)
}
}
});
}
indices.truncate(k);
indices
}
LocalSearchApplicationStrategy::Probabilistic { probability } => {
let mut rng = crate::rng::make_rng();
(0..offspring_buf.len())
.filter(|_| rng.random::<f64>() < probability)
.collect()
}
LocalSearchApplicationStrategy::EveryNGenerations { .. } => {
(0..offspring_buf.len()).collect()
}
};
let is_baldwinian = matches!(mode, LocalSearchMode::Baldwinian);
let original_dnas: Vec<Vec<U::Gene>> = if is_baldwinian {
candidates
.iter()
.map(|&idx| offspring_buf[idx].dna().to_vec())
.collect()
} else {
Vec::new()
};
let ff = Arc::clone(
self.fitness_fn
.as_ref()
.expect("Fitness function required when local search is configured"),
);
let search_method = ls_config.method;
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
{
let mut selected: Vec<U> = candidates
.iter()
.map(|&idx| offspring_buf[idx].clone())
.collect();
selected.par_iter_mut().for_each(|individual| {
let _ = search_method.improve(individual, ff.as_ref());
});
for (&idx, improved) in candidates.iter().zip(selected) {
offspring_buf[idx] = improved;
}
}
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
{
candidates.iter().for_each(|&idx| {
let _ = search_method.improve(&mut offspring_buf[idx], ff.as_ref());
});
}
if is_baldwinian {
for (orig_pos, &idx) in candidates.iter().enumerate() {
if let Some(orig_dna) = original_dnas.get(orig_pos) {
let improved_fitness = offspring_buf[idx].fitness();
offspring_buf[idx]
.set_dna(std::borrow::Cow::Owned(orig_dna.clone()));
offspring_buf[idx].set_fitness(improved_fitness);
}
}
}
}
}
self.population.add_chromosomes(&mut offspring_buf);
if let Some(ref mut hof) = self.hall_of_fame {
for c in self.population.chromosomes.iter() {
hof.try_insert(c, i as u64);
}
}
let elite: Vec<U> = if self.configuration.elitism_count > 0 {
let idx = generation::extract_elite(
&self.population.chromosomes,
self.configuration.elitism_count,
self.configuration.limit_configuration.problem_solving,
);
idx.iter()
.map(|&i| self.population.chromosomes[i].clone())
.collect()
} else {
Vec::new()
};
let t_surv: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
if let Some(penalty) = self.configuration.length_penalty {
survivor::apply_parsimony_pressure(
self.configuration.survivor,
&mut self.population.chromosomes,
initial_population_size,
self.configuration.limit_configuration,
penalty,
)?;
} else {
survivor::factory(
self.configuration.survivor,
&mut self.population.chromosomes,
initial_population_size,
self.configuration.limit_configuration,
)?;
}
if let Some(t) = t_surv {
let pop_size = self.population.chromosomes.len();
self.notify(|obs| obs.on_survivor_selection_complete(i, t.elapsed(), pop_size));
}
if !elite.is_empty() {
generation::reinsert_elite(
&mut self.population.chromosomes,
elite,
self.configuration.limit_configuration.problem_solving,
);
}
if self.configuration.adaptive_ga {
self.population.recalculate_aga();
}
let mut fitness_values: Vec<f64> = self
.population
.chromosomes
.iter()
.map(|c| c.fitness())
.collect();
if let Some(ref niching_config) = self.configuration.niching_configuration {
if niching_config.enabled {
let dna_slices: Vec<&[U::Gene]> = self
.population
.chromosomes
.iter()
.map(|c| c.dna())
.collect();
crate::niching::sharing::apply_fitness_sharing_with_dna(
&mut fitness_values,
&dna_slices,
|dna_a: &[U::Gene], dna_b: &[U::Gene]| {
let max_len = dna_a.len().max(dna_b.len());
if max_len == 0 {
return 0.0;
}
let mut diff = 0usize;
for idx in 0..max_len {
let id_a = dna_a.get(idx).map(|g| g.id()).unwrap_or(-1);
let id_b = dna_b.get(idx).map(|g| g.id()).unwrap_or(-1);
if id_a != id_b {
diff += 1;
}
}
diff as f64
},
niching_config.sigma_share,
niching_config.alpha,
);
for (chromosome, &shared_fitness) in self
.population
.chromosomes
.iter_mut()
.zip(fitness_values.iter())
{
chromosome.set_fitness(shared_fitness);
}
}
}
{
let ps = self.configuration.limit_configuration.problem_solving;
if !fitness_values.is_empty() {
let best_idx =
fitness_values
.iter()
.enumerate()
.fold(0usize, |best, (i, &f)| {
let best_f = fitness_values[best];
let is_better = match ps {
ProblemSolving::Maximization | ProblemSolving::FixedFitness => {
f > best_f
}
ProblemSolving::Minimization => f < best_f,
};
if is_better {
i
} else {
best
}
});
if !self.population.best_chromosome_is_set {
self.population.best_chromosome =
self.population.chromosomes[best_idx].clone();
self.population.best_chromosome_is_set = true;
} else {
let candidate = fitness_values[best_idx];
let current = self.population.best_chromosome.fitness();
let better = match ps {
ProblemSolving::Maximization | ProblemSolving::FixedFitness => {
candidate > current
}
ProblemSolving::Minimization => candidate < current,
};
if better {
self.population.best_chromosome =
self.population.chromosomes[best_idx].clone();
}
}
}
}
let mut gen_stats =
stats::collect_generation_stats(i, &fitness_values, is_maximization);
adaptive::update_dynamic_mutation(
&self.configuration.mutation_configuration,
&mut self.dynamic_mutation_probability,
&mut gen_stats,
);
cache::cache_fill_stats(
&self.fitness_cache,
&mut gen_stats,
prev_cache_hits,
prev_cache_misses,
)?;
gen_stats.true_fitness_calls = true_fitness_calls;
if let Some(ref ext_config) = self.configuration.extension_configuration {
if extension::should_trigger_extension(ext_config, gen_stats.diversity) {
extension_ops::factory(
ext_config.method,
&mut self.population.chromosomes,
initial_population_size,
self.configuration.limit_configuration.problem_solving,
ext_config,
)?;
self.notify(|obs| {
obs.on_extension_triggered(ExtensionEvent {
generation: i,
diversity: gen_stats.diversity,
extension_type: ext_config.method.as_str(),
threshold: ext_config.diversity_threshold,
})
});
if self.population.chromosomes.len() < initial_population_size {
if let Some(ref init_fn) = self.initialization_fn {
let deficit =
initial_population_size - self.population.chromosomes.len();
let chromosome_length =
self.configuration.limit_configuration.chromosome_length;
let alleles_ref: Option<&[U::Gene]> = if self.alleles.is_empty() {
None
} else {
Some(self.alleles.as_slice())
};
let ff = self.fitness_fn.as_ref().map(Arc::clone);
let (min_obs, max_obs): (usize, usize) = match chromosome_length {
crate::chromosomes::ChromosomeLength::Variable { min, max } => {
let observed_min = self
.population
.chromosomes
.iter()
.map(|c| c.dna().len())
.min()
.unwrap_or(min);
let observed_max = self
.population
.chromosomes
.iter()
.map(|c| c.dna().len())
.max()
.unwrap_or(max);
(observed_min.max(min), observed_max.min(max))
}
crate::chromosomes::ChromosomeLength::Fixed(n) => (n, n),
};
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
let new_chromosomes: Vec<U> = (0..deficit)
.into_par_iter()
.map(|_| {
let len = if min_obs == max_obs {
min_obs
} else {
let mut rng = crate::rng::make_rng();
rng.random_range(min_obs..=max_obs)
};
let genes = init_fn(len, alleles_ref);
let mut new_chromosome = U::new();
new_chromosome.set_dna(std::borrow::Cow::Owned(genes));
if let Some(ref ff) = ff {
let ff_clone = Arc::clone(ff);
new_chromosome.set_fitness_fn(move |genes| ff_clone(genes));
}
new_chromosome.calculate_fitness();
new_chromosome.set_age(0);
new_chromosome
})
.collect();
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
let new_chromosomes: Vec<U> = (0..deficit)
.map(|_| {
let len = if min_obs == max_obs {
min_obs
} else {
let mut rng = crate::rng::make_rng();
rng.random_range(min_obs..=max_obs)
};
let genes = init_fn(len, alleles_ref);
let mut new_chromosome = U::new();
new_chromosome.set_dna(std::borrow::Cow::Owned(genes));
if let Some(ref ff) = ff {
let ff_clone = Arc::clone(ff);
new_chromosome.set_fitness_fn(move |genes| ff_clone(genes));
}
new_chromosome.calculate_fitness();
new_chromosome.set_age(0);
new_chromosome
})
.collect();
self.population.chromosomes.extend(new_chromosomes);
}
}
for c in self.population.chromosomes.iter_mut() {
if c.fitness().is_nan() {
c.calculate_fitness();
}
}
}
}
self.stats.push(gen_stats);
let notify_stats = self.stats.last().unwrap().clone();
self.notify(|obs| obs.on_generation_end(¬ify_stats));
#[cfg(feature = "serde")]
{
let spc = &self.configuration.save_progress_configuration;
if spc.save_progress
&& spc.save_progress_interval > 0
&& (i + 1) % spc.save_progress_interval == 0
{
let ckpt = crate::checkpoint::Checkpoint {
population: self.population.clone(),
configuration: self.configuration.clone(),
generation: i,
stats: self.stats.clone(),
};
let path = std::path::Path::new(&spc.save_progress_path)
.join(format!("checkpoint_gen_{}.json", i + 1));
if let Err(e) = crate::checkpoint::save_checkpoint(&ckpt, &path) {
crate::log_warn!(
"Failed to save checkpoint at generation {}: {}",
i + 1,
e
);
}
}
}
if let Some(func) = &callback {
if (generation_callback_count + 1) == generations_to_callback {
if func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
)
.is_break()
{
self.termination_cause = TerminationCause::CallbackRequested;
break;
}
generation_callback_count = 0;
} else {
generation_callback_count += 1;
}
}
if stopping::limit_reached(
self.configuration.limit_configuration,
&self.population.chromosomes,
) {
self.termination_cause = TerminationCause::FitnessTargetReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
let current_best = self.population.best_chromosome.fitness();
let improved = match self.configuration.limit_configuration.problem_solving {
ProblemSolving::Maximization => current_best > best_fitness_so_far,
ProblemSolving::Minimization => current_best < best_fitness_so_far,
_ => (current_best - best_fitness_so_far).abs() > f64::EPSILON,
};
if improved {
best_fitness_so_far = current_best;
stagnation_count = 0;
self.notify(|obs| obs.on_new_best(i, &self.population.best_chromosome));
} else {
stagnation_count += 1;
self.notify(|obs| obs.on_stagnation(i, stagnation_count));
}
if let Some(max_stagnation) = self.configuration.stagnation_generations {
if stagnation_count >= max_stagnation {
self.termination_cause = TerminationCause::StagnationReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
}
if let Some(threshold) = self.configuration.convergence_threshold {
if self.stats.last().unwrap().fitness_std_dev < threshold {
self.termination_cause = TerminationCause::ConvergenceReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(max_secs) = self.configuration.max_duration_secs {
if start_time.elapsed().as_secs_f64() >= max_secs {
self.termination_cause = TerminationCause::TimeLimitReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
}
}
if self.termination_cause == TerminationCause::NotTerminated {
self.termination_cause = TerminationCause::GenerationLimitReached;
}
self.notify(|obs| obs.on_run_end(self.termination_cause, &self.stats));
if let Some(func) = &callback {
if self.termination_cause == TerminationCause::GenerationLimitReached {
let final_stats = self.stats.last().cloned().unwrap_or_else(|| {
GenerationStats::from_fitness_values(0, &[], is_maximization)
});
let _ = func(
&self.configuration.limit_configuration.max_generations,
&self.population,
&final_stats,
&self.termination_cause,
);
}
}
Ok(&self.population)
}
pub fn stats(&self) -> &[GenerationStats] {
&self.stats
}
pub fn hall_of_fame(&self) -> Option<&HallOfFame<U>> {
self.hall_of_fame.as_ref()
}
fn process_constraints_population(&mut self, generation: usize) -> Result<(), GaError> {
let constraint_fns = match self.constraint_fns {
Some(ref fns) => fns,
None => return Ok(()),
};
let violations: Vec<f64> = self
.population
.chromosomes
.iter()
.map(|c| {
let dna = c.dna();
constraint_fns.iter().map(|f| f(dna)).sum()
})
.collect();
match self.constraint_handling {
Some(ConstraintHandling::FeasibilityRules) => {
self.apply_feasibility_rules(&violations);
}
None => {
self.apply_penalty_to_chromosomes(&violations, generation);
}
}
Ok(())
}
fn apply_feasibility_rules(&mut self, violations: &[f64]) {
let (feasible_count, worst_feasible) = {
let mut wf = f64::NEG_INFINITY;
let mut bf = f64::INFINITY;
let mut count = 0usize;
for (c, &v) in self.population.chromosomes.iter().zip(violations.iter()) {
if v <= 0.0 {
count += 1;
let f = c.fitness();
if f > wf {
wf = f;
}
if f < bf {
bf = f;
}
}
}
(count, wf)
};
let is_maximization = matches!(
self.configuration.limit_configuration.problem_solving,
ProblemSolving::Maximization
);
for (c, &v) in self
.population
.chromosomes
.iter_mut()
.zip(violations.iter())
{
if v > 0.0 {
if feasible_count > 0 {
if is_maximization {
c.set_fitness(worst_feasible - v);
} else {
c.set_fitness(worst_feasible + v);
}
} else {
if is_maximization {
c.set_fitness(-v);
} else {
c.set_fitness(v);
}
}
}
}
}
fn apply_penalty_to_chromosomes(&mut self, violations: &[f64], generation: usize) {
match self.penalty_strategy {
PenaltyStrategy::None => {}
PenaltyStrategy::Static { coefficient } => {
for (c, &v) in self
.population
.chromosomes
.iter_mut()
.zip(violations.iter())
{
if v > 0.0 {
c.set_fitness(c.fitness() + coefficient * v);
}
}
}
PenaltyStrategy::Dynamic { c, alpha, beta } => {
for (chr, &v) in self
.population
.chromosomes
.iter_mut()
.zip(violations.iter())
{
if v > 0.0 {
let raw = chr.fitness();
let penalized = crate::constraints::apply_dynamic_penalty(
raw, v, generation, c, alpha, beta,
);
chr.set_fitness(penalized);
}
}
}
PenaltyStrategy::Adaptive {
initial_coefficient,
window_size,
} => {
if self.penalty_coefficient == 0.0 {
self.penalty_coefficient = initial_coefficient;
}
let coeff = self.penalty_coefficient;
if generation > 0 && generation % window_size == 0 {
let best_violation = violations
.iter()
.zip(self.population.chromosomes.iter())
.find(|(_, c)| {
c.fitness()
== self
.population
.chromosomes
.iter()
.map(|x| x.fitness())
.fold(f64::NEG_INFINITY, |a, b| a.max(b))
})
.map(|(v, _)| *v)
.unwrap_or(0.0);
self.adaptive_penalty_counter = if best_violation <= 0.0 {
self.adaptive_penalty_counter + 1
} else {
self.adaptive_penalty_counter - 1
};
if self.adaptive_penalty_counter > 0 {
let new_coeff = self.penalty_coefficient * 1.1;
self.penalty_coefficient = new_coeff;
} else if self.adaptive_penalty_counter < 0 {
let new_coeff = (self.penalty_coefficient / 1.1).max(0.001);
self.penalty_coefficient = new_coeff;
}
}
for (c, &v) in self
.population
.chromosomes
.iter_mut()
.zip(violations.iter())
{
if v > 0.0 {
c.set_fitness(c.fitness() + coeff * v);
}
}
}
}
}
}
impl<U> Strategy<U> for Ga<U>
where
U: LinearChromosome
+ Send
+ Sync
+ 'static
+ Clone
+ Debug
+ mutation::ValueMutable
+ MaybeSerialize
+ MaybeDeserialize
+ OperatorCompat
+ crate::traits::RealValuedMutation,
U::Gene: 'static + Debug,
{
fn run(&mut self) -> Result<(), GaError> {
Ga::run(self).map(|_| ())
}
fn best(&self) -> Option<&U> {
if self.population.best_chromosome_is_set {
Some(&self.population.best_chromosome)
} else {
None
}
}
}
impl<U> Ga<U>
where
U: LinearChromosome
+ VectorFitness
+ Send
+ Sync
+ 'static
+ Clone
+ Debug
+ mutation::ValueMutable
+ MaybeSerialize
+ MaybeDeserialize
+ OperatorCompat
+ crate::traits::RealValuedMutation,
U::Gene: 'static + Debug,
{
pub fn select_parents_lexicase(&mut self) -> Result<Vec<Vec<usize>>, GaError> {
crate::operations::selection::factory_lexicase(
&mut self.population.chromosomes,
self.configuration.selection_configuration,
self.configuration.number_of_threads,
)
}
pub fn run_lexicase(&mut self) -> Result<&Population<U>, GaError> {
self.run_lexicase_with_callback(
None::<
fn(&usize, &Population<U>, &GenerationStats, &TerminationCause) -> ControlFlow<()>,
>,
0,
)
}
pub fn run_lexicase_with_callback<F>(
&mut self,
callback: Option<F>,
generations_to_callback: usize,
) -> Result<&Population<U>, GaError>
where
U: LinearChromosome + Send + Sync + 'static + Clone + MaybeDeserialize,
F: Fn(&usize, &Population<U>, &GenerationStats, &TerminationCause) -> ControlFlow<()>,
{
ValidatorFactory::validate::<U>(Some(&self.configuration), None, Some(&self.alleles))?;
crate::rng::set_seed(self.configuration.rng_seed);
#[cfg_attr(not(feature = "serde"), allow(unused_mut))]
let mut checkpoint_generation: Option<usize> = None;
if self.checkpoint_path.is_some() {
#[cfg(feature = "serde")]
{
let path = self.checkpoint_path.take().unwrap();
let ckpt = crate::checkpoint::load_checkpoint::<U>(&path).map_err(|e| {
GaError::CheckpointError(format!(
"Failed to load checkpoint '{}': {}",
path.display(),
e
))
})?;
let builder_selection = self.configuration.selection_configuration.method;
let builder_crossover = self.configuration.crossover_configuration.method;
let builder_mutation = self.configuration.mutation_configuration.method;
let builder_survivor = self.configuration.survivor;
let builder_problem_solving =
self.configuration.limit_configuration.problem_solving;
let builder_max_generations =
self.configuration.limit_configuration.max_generations;
let builder_population_size =
self.configuration.limit_configuration.population_size;
self.configuration = ckpt.configuration;
self.configuration.selection_configuration.method = builder_selection;
self.configuration.crossover_configuration.method = builder_crossover;
self.configuration.mutation_configuration.method = builder_mutation;
self.configuration.survivor = builder_survivor;
self.configuration.limit_configuration.problem_solving = builder_problem_solving;
self.configuration.limit_configuration.max_generations = builder_max_generations;
self.configuration.limit_configuration.population_size = builder_population_size;
self.population = ckpt.population;
self.stats = ckpt.stats; checkpoint_generation = Some(ckpt.generation);
}
#[cfg(not(feature = "serde"))]
{
return Err(GaError::CheckpointError(
"Checkpoint loading requires the 'serde' feature. Enable it in Cargo.toml: genetic_algorithms = { features = [\"serde\"] }".to_string(),
));
}
} else if self.population.size() == 0 && self.initialization_fn.is_some() {
self.initialization()?;
if let Some(eval) = self.batch_evaluator.as_ref().map(Arc::clone) {
let cache = self.fitness_cache.as_ref().map(Arc::clone);
batch::batch_evaluate(eval, cache, &mut self.population.chromosomes)?;
}
} else if self.population.size() == 0 && self.initialization_fn.is_none() {
return Err(GaError::InitializationError(
"No initialization function set".to_string(),
));
}
if self.configuration.adaptive_ga {
self.population.recalculate_aga();
}
aos::init_aos_state(
&self.configuration,
&mut self.aos_crossover,
&mut self.aos_mutation,
);
if self.configuration.mutation_configuration.dynamic_mutation {
self.dynamic_mutation_probability = self
.configuration
.mutation_configuration
.probability_max
.unwrap_or(1.0);
}
if self.batch_evaluator.is_some() && self.fitness_cache.is_none() {
if let Some(size) = self.fitness_cache_size {
self.fitness_cache = Some(Arc::new(Mutex::new(
crate::fitness::cache::FitnessCache::new(size),
)));
}
}
let initial_population_size = self.population.size();
let mut age = 0usize;
self.population.fitness_calculation(
self.configuration.number_of_threads,
self.configuration.limit_configuration.problem_solving,
);
if self.constraint_fns.is_some() {
self.process_constraints_population(0)?;
}
let mut generation_callback_count = 0usize;
if checkpoint_generation.is_none() {
self.stats.clear();
}
let is_maximization = matches!(
self.configuration.limit_configuration.problem_solving,
ProblemSolving::Maximization
);
#[cfg(not(target_arch = "wasm32"))]
let start_time = Instant::now();
#[cfg(target_arch = "wasm32")]
if self.configuration.max_duration_secs.is_some() {
crate::log_warn!(target: "ga_events", "max_duration_secs is not supported on wasm32 — time limit will be ignored");
}
let mut best_fitness_so_far = self.population.best_chromosome.fitness();
let mut stagnation_count: usize = 0;
self.notify(|obs| obs.on_run_start());
let start_gen = checkpoint_generation.unwrap_or(0);
let total_gens = if checkpoint_generation.is_some() {
start_gen + self.configuration.limit_configuration.max_generations
} else {
self.configuration.limit_configuration.max_generations
};
let mut offspring_buf: Vec<U> =
Vec::with_capacity(self.configuration.limit_configuration.population_size * 2);
for i in start_gen..total_gens {
age += 1;
let (prev_cache_hits, prev_cache_misses) = cache::cache_snapshot(&self.fitness_cache)?;
self.notify(|obs| obs.on_generation_start(i));
let t_sel: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let parents = crate::operations::selection::factory_lexicase(
&mut self.population.chromosomes,
self.configuration.selection_configuration,
self.configuration.number_of_threads,
)?;
if let Some(t) = t_sel {
self.notify(|obs| obs.on_selection_complete(i, t.elapsed(), parents.len()));
}
let dynamic_prob = if self.configuration.mutation_configuration.dynamic_mutation {
Some(self.dynamic_mutation_probability)
} else {
None
};
let t_cx: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let crossover_fitness_fn = if self.batch_evaluator.is_some() {
None
} else {
self.fitness_fn.clone()
};
generation::parent_crossover(
&parents,
&self.population.chromosomes,
&self.configuration,
generation::ParentCrossoverParams {
age,
f_max: self.population.f_max,
f_avg: self.population.f_avg,
dynamic_mutation_prob: dynamic_prob,
generation: i,
best_fitness: best_fitness_so_far,
is_maximization,
fitness_fn: crossover_fitness_fn,
crossover_portfolio: self.configuration.crossover_portfolio.as_ref(),
mutation_portfolio: self.configuration.mutation_portfolio.as_ref(),
aos_crossover_state: self.aos_crossover.as_ref(),
aos_mutation_state: self.aos_mutation.as_ref(),
},
&mut offspring_buf,
)?;
let true_fitness_calls: Option<u64> = if let Some((ref surrogate, fraction)) =
self.surrogate
{
if offspring_buf.is_empty() {
Some(0)
} else {
let mut scores: Vec<(usize, f64)> = offspring_buf
.iter()
.enumerate()
.map(|(idx, c)| {
let raw = surrogate.predict(c);
let score = if raw.is_nan() { f64::NEG_INFINITY } else { raw };
(idx, score)
})
.collect();
scores.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let keep = ((offspring_buf.len() as f64 * fraction).floor() as usize).max(1);
scores.truncate(keep);
scores.sort_unstable_by_key(|&(idx, _)| idx);
offspring_buf = scores
.into_iter()
.map(|(idx, _)| offspring_buf[idx].clone())
.collect();
Some(offspring_buf.len() as u64)
}
} else {
None
};
if let Some(eval) = self.batch_evaluator.as_ref().map(Arc::clone) {
let cache = self.fitness_cache.as_ref().map(Arc::clone);
batch::batch_evaluate(eval, cache, &mut offspring_buf)?;
}
if let Some(t) = t_cx {
let elapsed = t.elapsed();
let offspring_count = offspring_buf.len();
let pop_size = self.population.chromosomes.len();
self.notify(|obs| obs.on_crossover_complete(i, elapsed, offspring_count));
self.notify(|obs| obs.on_mutation_complete(i, elapsed, pop_size));
self.notify(|obs| obs.on_fitness_evaluation_complete(i, elapsed, pop_size));
}
if let Some(ref repair_op) = self.repair_operator {
for c in offspring_buf.iter_mut() {
repair_op(c)?;
c.calculate_fitness();
}
}
if let Some(ref constraint_fns) = self.constraint_fns {
for c in offspring_buf.iter_mut() {
let dna = c.dna();
let total_viol: f64 = constraint_fns.iter().map(|f| f(dna)).sum();
if total_viol > 0.0 {
match self.penalty_strategy {
PenaltyStrategy::None => {}
PenaltyStrategy::Static { coefficient } => {
c.set_fitness(c.fitness() + coefficient * total_viol);
}
PenaltyStrategy::Dynamic { c: dc, alpha, beta } => {
let penalized = crate::constraints::apply_dynamic_penalty(
c.fitness(),
total_viol,
age,
dc,
alpha,
beta,
);
c.set_fitness(penalized);
}
PenaltyStrategy::Adaptive {
initial_coefficient,
..
} => {
let coeff = if self.penalty_coefficient == 0.0 {
initial_coefficient
} else {
self.penalty_coefficient
};
c.set_fitness(c.fitness() + coeff * total_viol);
}
}
}
}
}
if let Some(ref ls_config) = self.configuration.local_search_configuration {
let strategy = ls_config.application_strategy;
let mode = ls_config.mode;
let should_apply = match strategy {
LocalSearchApplicationStrategy::EveryNGenerations { interval } => {
interval == 0 || (i + 1) % interval == 0
}
_ => true,
};
if should_apply && !offspring_buf.is_empty() {
let candidates: Vec<usize> = match strategy {
LocalSearchApplicationStrategy::AllOffspring => {
(0..offspring_buf.len()).collect()
}
LocalSearchApplicationStrategy::BestN { n } => {
let mut indices: Vec<usize> = (0..offspring_buf.len()).collect();
let ps = self.configuration.limit_configuration.problem_solving;
let k = n.min(indices.len());
if k > 0 {
indices.select_nth_unstable_by(k.saturating_sub(1), |&a, &b| {
let (fa, fb) =
(offspring_buf[a].fitness(), offspring_buf[b].fitness());
match ps {
ProblemSolving::Minimization
| ProblemSolving::FixedFitness => {
fa.partial_cmp(&fb).unwrap_or(std::cmp::Ordering::Equal)
}
ProblemSolving::Maximization => {
fb.partial_cmp(&fa).unwrap_or(std::cmp::Ordering::Equal)
}
}
});
}
indices.truncate(k);
indices
}
LocalSearchApplicationStrategy::Probabilistic { probability } => {
let mut rng = crate::rng::make_rng();
(0..offspring_buf.len())
.filter(|_| rng.random::<f64>() < probability)
.collect()
}
LocalSearchApplicationStrategy::EveryNGenerations { .. } => {
(0..offspring_buf.len()).collect()
}
};
let is_baldwinian = matches!(mode, LocalSearchMode::Baldwinian);
let original_dnas: Vec<Vec<U::Gene>> = if is_baldwinian {
candidates
.iter()
.map(|&idx| offspring_buf[idx].dna().to_vec())
.collect()
} else {
Vec::new()
};
let ff = Arc::clone(
self.fitness_fn
.as_ref()
.expect("Fitness function required when local search is configured"),
);
let search_method = ls_config.method;
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
{
let mut selected: Vec<U> = candidates
.iter()
.map(|&idx| offspring_buf[idx].clone())
.collect();
selected.par_iter_mut().for_each(|individual| {
let _ = search_method.improve(individual, ff.as_ref());
});
for (&idx, improved) in candidates.iter().zip(selected) {
offspring_buf[idx] = improved;
}
}
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
{
candidates.iter().for_each(|&idx| {
let _ = search_method.improve(&mut offspring_buf[idx], ff.as_ref());
});
}
if is_baldwinian {
for (orig_pos, &idx) in candidates.iter().enumerate() {
if let Some(orig_dna) = original_dnas.get(orig_pos) {
let improved_fitness = offspring_buf[idx].fitness();
offspring_buf[idx]
.set_dna(std::borrow::Cow::Owned(orig_dna.clone()));
offspring_buf[idx].set_fitness(improved_fitness);
}
}
}
}
}
self.population.add_chromosomes(&mut offspring_buf);
if let Some(ref mut hof) = self.hall_of_fame {
for c in self.population.chromosomes.iter() {
hof.try_insert(c, i as u64);
}
}
let elite: Vec<U> = if self.configuration.elitism_count > 0 {
let idx = generation::extract_elite(
&self.population.chromosomes,
self.configuration.elitism_count,
self.configuration.limit_configuration.problem_solving,
);
idx.iter()
.map(|&i| self.population.chromosomes[i].clone())
.collect()
} else {
Vec::new()
};
let t_surv: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
if let Some(penalty) = self.configuration.length_penalty {
survivor::apply_parsimony_pressure(
self.configuration.survivor,
&mut self.population.chromosomes,
initial_population_size,
self.configuration.limit_configuration,
penalty,
)?;
} else {
survivor::factory(
self.configuration.survivor,
&mut self.population.chromosomes,
initial_population_size,
self.configuration.limit_configuration,
)?;
}
if let Some(t) = t_surv {
let pop_size = self.population.chromosomes.len();
self.notify(|obs| obs.on_survivor_selection_complete(i, t.elapsed(), pop_size));
}
if !elite.is_empty() {
generation::reinsert_elite(
&mut self.population.chromosomes,
elite,
self.configuration.limit_configuration.problem_solving,
);
}
if self.configuration.adaptive_ga {
self.population.recalculate_aga();
}
let mut fitness_values: Vec<f64> = self
.population
.chromosomes
.iter()
.map(|c| c.fitness())
.collect();
if let Some(ref niching_config) = self.configuration.niching_configuration {
if niching_config.enabled {
let dna_slices: Vec<&[U::Gene]> = self
.population
.chromosomes
.iter()
.map(|c| c.dna())
.collect();
crate::niching::sharing::apply_fitness_sharing_with_dna(
&mut fitness_values,
&dna_slices,
|dna_a: &[U::Gene], dna_b: &[U::Gene]| {
let max_len = dna_a.len().max(dna_b.len());
if max_len == 0 {
return 0.0;
}
let mut diff = 0usize;
for idx in 0..max_len {
let id_a = dna_a.get(idx).map(|g| g.id()).unwrap_or(-1);
let id_b = dna_b.get(idx).map(|g| g.id()).unwrap_or(-1);
if id_a != id_b {
diff += 1;
}
}
diff as f64
},
niching_config.sigma_share,
niching_config.alpha,
);
for (chromosome, &shared_fitness) in self
.population
.chromosomes
.iter_mut()
.zip(fitness_values.iter())
{
chromosome.set_fitness(shared_fitness);
}
}
}
{
let ps = self.configuration.limit_configuration.problem_solving;
if !fitness_values.is_empty() {
let best_idx =
fitness_values
.iter()
.enumerate()
.fold(0usize, |best, (i, &f)| {
let best_f = fitness_values[best];
let is_better = match ps {
ProblemSolving::Maximization | ProblemSolving::FixedFitness => {
f > best_f
}
ProblemSolving::Minimization => f < best_f,
};
if is_better {
i
} else {
best
}
});
if !self.population.best_chromosome_is_set {
self.population.best_chromosome =
self.population.chromosomes[best_idx].clone();
self.population.best_chromosome_is_set = true;
} else {
let candidate = fitness_values[best_idx];
let current = self.population.best_chromosome.fitness();
let better = match ps {
ProblemSolving::Maximization | ProblemSolving::FixedFitness => {
candidate > current
}
ProblemSolving::Minimization => candidate < current,
};
if better {
self.population.best_chromosome =
self.population.chromosomes[best_idx].clone();
}
}
}
}
let mut gen_stats =
stats::collect_generation_stats(i, &fitness_values, is_maximization);
adaptive::update_dynamic_mutation(
&self.configuration.mutation_configuration,
&mut self.dynamic_mutation_probability,
&mut gen_stats,
);
cache::cache_fill_stats(
&self.fitness_cache,
&mut gen_stats,
prev_cache_hits,
prev_cache_misses,
)?;
gen_stats.true_fitness_calls = true_fitness_calls;
if let Some(ref ext_config) = self.configuration.extension_configuration {
if extension::should_trigger_extension(ext_config, gen_stats.diversity) {
extension_ops::factory(
ext_config.method,
&mut self.population.chromosomes,
initial_population_size,
self.configuration.limit_configuration.problem_solving,
ext_config,
)?;
self.notify(|obs| {
obs.on_extension_triggered(ExtensionEvent {
generation: i,
diversity: gen_stats.diversity,
extension_type: ext_config.method.as_str(),
threshold: ext_config.diversity_threshold,
})
});
if self.population.chromosomes.len() < initial_population_size {
if let Some(ref init_fn) = self.initialization_fn {
let deficit =
initial_population_size - self.population.chromosomes.len();
let chromosome_length =
self.configuration.limit_configuration.chromosome_length;
let alleles_ref: Option<&[U::Gene]> = if self.alleles.is_empty() {
None
} else {
Some(self.alleles.as_slice())
};
let ff = self.fitness_fn.as_ref().map(Arc::clone);
let (min_obs, max_obs): (usize, usize) = match chromosome_length {
crate::chromosomes::ChromosomeLength::Variable { min, max } => {
let observed_min = self
.population
.chromosomes
.iter()
.map(|c| c.dna().len())
.min()
.unwrap_or(min);
let observed_max = self
.population
.chromosomes
.iter()
.map(|c| c.dna().len())
.max()
.unwrap_or(max);
(observed_min.max(min), observed_max.min(max))
}
crate::chromosomes::ChromosomeLength::Fixed(n) => (n, n),
};
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
let new_chromosomes: Vec<U> = (0..deficit)
.into_par_iter()
.map(|_| {
let len = if min_obs == max_obs {
min_obs
} else {
let mut rng = crate::rng::make_rng();
rng.random_range(min_obs..=max_obs)
};
let genes = init_fn(len, alleles_ref);
let mut new_chromosome = U::new();
new_chromosome.set_dna(std::borrow::Cow::Owned(genes));
if let Some(ref ff) = ff {
let ff_clone = Arc::clone(ff);
new_chromosome.set_fitness_fn(move |genes| ff_clone(genes));
}
new_chromosome.calculate_fitness();
new_chromosome.set_age(0);
new_chromosome
})
.collect();
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
let new_chromosomes: Vec<U> = (0..deficit)
.map(|_| {
let len = if min_obs == max_obs {
min_obs
} else {
let mut rng = crate::rng::make_rng();
rng.random_range(min_obs..=max_obs)
};
let genes = init_fn(len, alleles_ref);
let mut new_chromosome = U::new();
new_chromosome.set_dna(std::borrow::Cow::Owned(genes));
if let Some(ref ff) = ff {
let ff_clone = Arc::clone(ff);
new_chromosome.set_fitness_fn(move |genes| ff_clone(genes));
}
new_chromosome.calculate_fitness();
new_chromosome.set_age(0);
new_chromosome
})
.collect();
self.population.chromosomes.extend(new_chromosomes);
}
}
for c in self.population.chromosomes.iter_mut() {
if c.fitness().is_nan() {
c.calculate_fitness();
}
}
}
}
self.stats.push(gen_stats);
let notify_stats = self.stats.last().unwrap().clone();
self.notify(|obs| obs.on_generation_end(¬ify_stats));
#[cfg(feature = "serde")]
{
let spc = &self.configuration.save_progress_configuration;
if spc.save_progress
&& spc.save_progress_interval > 0
&& (i + 1) % spc.save_progress_interval == 0
{
let ckpt = crate::checkpoint::Checkpoint {
population: self.population.clone(),
configuration: self.configuration.clone(),
generation: i,
stats: self.stats.clone(),
};
let path = std::path::Path::new(&spc.save_progress_path)
.join(format!("checkpoint_gen_{}.json", i + 1));
if let Err(e) = crate::checkpoint::save_checkpoint(&ckpt, &path) {
crate::log_warn!(
"Failed to save checkpoint at generation {}: {}",
i + 1,
e
);
}
}
}
if let Some(func) = &callback {
if (generation_callback_count + 1) == generations_to_callback {
if func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
)
.is_break()
{
self.termination_cause = TerminationCause::CallbackRequested;
break;
}
generation_callback_count = 0;
} else {
generation_callback_count += 1;
}
}
if stopping::limit_reached(
self.configuration.limit_configuration,
&self.population.chromosomes,
) {
self.termination_cause = TerminationCause::FitnessTargetReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
let current_best = self.population.best_chromosome.fitness();
let improved = match self.configuration.limit_configuration.problem_solving {
ProblemSolving::Maximization => current_best > best_fitness_so_far,
ProblemSolving::Minimization => current_best < best_fitness_so_far,
_ => (current_best - best_fitness_so_far).abs() > f64::EPSILON,
};
if improved {
best_fitness_so_far = current_best;
stagnation_count = 0;
self.notify(|obs| obs.on_new_best(i, &self.population.best_chromosome));
} else {
stagnation_count += 1;
self.notify(|obs| obs.on_stagnation(i, stagnation_count));
}
if let Some(max_stagnation) = self.configuration.stagnation_generations {
if stagnation_count >= max_stagnation {
self.termination_cause = TerminationCause::StagnationReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
}
if let Some(threshold) = self.configuration.convergence_threshold {
if self.stats.last().unwrap().fitness_std_dev < threshold {
self.termination_cause = TerminationCause::ConvergenceReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(max_secs) = self.configuration.max_duration_secs {
if start_time.elapsed().as_secs_f64() >= max_secs {
self.termination_cause = TerminationCause::TimeLimitReached;
if let Some(func) = &callback {
let _ = func(
&i,
&self.population,
self.stats.last().unwrap(),
&self.termination_cause,
);
}
break;
}
}
}
if self.termination_cause == TerminationCause::NotTerminated {
self.termination_cause = TerminationCause::GenerationLimitReached;
}
self.notify(|obs| obs.on_run_end(self.termination_cause, &self.stats));
if let Some(func) = &callback {
if self.termination_cause == TerminationCause::GenerationLimitReached {
let final_stats = self.stats.last().cloned().unwrap_or_else(|| {
GenerationStats::from_fitness_values(0, &[], is_maximization)
});
let _ = func(
&self.configuration.limit_configuration.max_generations,
&self.population,
&final_stats,
&self.termination_cause,
);
}
}
Ok(&self.population)
}
}