use genetic_algorithms::traits::{ChromosomeT, GeneT};
use genetic_algorithms::SurrogateModel;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use genetic_algorithms::chromosomes::Range as RangeChromosome;
use genetic_algorithms::configuration::ProblemSolving;
use genetic_algorithms::ga::Ga;
use genetic_algorithms::genotypes::Range as RangeGenotype;
use genetic_algorithms::initializers::range_random_initialization;
use genetic_algorithms::operations::{Crossover, GaussianParams, Mutation, Selection, Survivor};
use genetic_algorithms::traits::{
ConfigurationT, CrossoverConfig, MutationConfig, SelectionConfig, StoppingConfig,
};
use genetic_algorithms::ChromosomeLength;
struct ZeroSurrogate;
impl SurrogateModel<RangeChromosome<f64>> for ZeroSurrogate {
fn predict(&self, _chromosome: &RangeChromosome<f64>) -> f64 {
0.0
}
}
#[derive(Debug, Clone, Default)]
struct StubGene {
id: i32,
}
impl GeneT for StubGene {
fn id(&self) -> i32 {
self.id
}
fn set_id(&mut self, id: i32) -> &mut Self {
self.id = id;
self
}
}
#[derive(Debug, Clone, Default)]
struct StubChromosome {
fitness: f64,
age: usize,
}
impl ChromosomeT for StubChromosome {
type Gene = StubGene;
fn fitness(&self) -> f64 {
self.fitness
}
fn set_fitness(&mut self, fitness: f64) -> &mut Self {
self.fitness = fitness;
self
}
fn calculate_fitness(&mut self) {
}
fn age(&self) -> usize {
self.age
}
fn set_age(&mut self, age: usize) -> &mut Self {
self.age = age;
self
}
}
struct CountingSurrogate {
calls: Arc<AtomicUsize>,
}
impl SurrogateModel<StubChromosome> for CountingSurrogate {
fn predict(&self, _chromosome: &StubChromosome) -> f64 {
self.calls.fetch_add(1, Ordering::SeqCst);
1.0
}
}
#[test]
fn test_predict_called() {
let calls = Arc::new(AtomicUsize::new(0));
let surrogate = CountingSurrogate {
calls: Arc::clone(&calls),
};
for _ in 0..5 {
let c = StubChromosome::default();
surrogate.predict(&c);
}
assert_eq!(
calls.load(Ordering::SeqCst),
5,
"predict must be called exactly 5 times"
);
let _shared: Arc<dyn SurrogateModel<StubChromosome> + Send + Sync> =
Arc::new(CountingSurrogate {
calls: Arc::new(AtomicUsize::new(0)),
});
}
fn floor_keep(n: usize, f: f64) -> usize {
((n as f64 * f).floor() as usize).max(1)
}
#[test]
fn test_prescreening_floor() {
assert_eq!(
floor_keep(10, 0.0001),
1,
"floor(10 * 0.0001) == 0, max(0,1) == 1"
);
assert_eq!(floor_keep(10, 0.5), 5, "floor(10 * 0.5) == 5");
assert_eq!(floor_keep(10, 1.0), 10, "floor(10 * 1.0) == 10");
assert_eq!(
floor_keep(10, 0.0),
1,
"floor(10 * 0.0) == 0, max(0,1) == 1"
);
assert_eq!(floor_keep(100, 0.3), 30, "floor(100 * 0.3) == 30");
assert_eq!(floor_keep(1, 0.1), 1, "max(floor(0.1), 1) == 1");
}
struct NanSurrogate {
nan_index: usize,
call_count: AtomicUsize,
}
impl SurrogateModel<StubChromosome> for NanSurrogate {
fn predict(&self, _chromosome: &StubChromosome) -> f64 {
let idx = self.call_count.fetch_add(1, Ordering::SeqCst);
if idx == self.nan_index {
f64::NAN
} else {
idx as f64
}
}
}
#[test]
fn test_nan_prediction_treated_as_worst() {
let offspring: Vec<StubChromosome> = (0..5).map(|_| StubChromosome::default()).collect();
let surrogate = NanSurrogate {
nan_index: 2, call_count: AtomicUsize::new(0),
};
let mut scored: Vec<(usize, f64)> = offspring
.iter()
.enumerate()
.map(|(i, c)| {
let raw = surrogate.predict(c);
let score = if raw.is_nan() { f64::NEG_INFINITY } else { raw };
(i, score)
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let last_original_index = scored.last().unwrap().0;
assert_eq!(
last_original_index, 2,
"NaN entry (original index 2) must sort last (treated as NEG_INFINITY)"
);
for (orig_idx, score) in &scored[..scored.len() - 1] {
assert!(
score.is_finite(),
"entry {orig_idx} should have a finite score, got {score}"
);
}
assert_eq!(
scored.last().unwrap().1,
f64::NEG_INFINITY,
"substituted NaN must be NEG_INFINITY"
);
}
#[cfg(feature = "serde")]
#[test]
fn stats_serde_default() {
use genetic_algorithms::stats::GenerationStats;
let json = r#"{
"generation": 0,
"best_fitness": 1.0,
"worst_fitness": 3.0,
"avg_fitness": 2.0,
"fitness_std_dev": 0.816,
"population_size": 3,
"diversity": 0.816,
"dynamic_mutation_probability": null,
"avg_node_count": 0.0,
"cache_hits": null,
"cache_misses": null
}"#;
let parsed: GenerationStats = serde_json::from_str(json)
.expect("GenerationStats must deserialise from JSON lacking true_fitness_calls");
assert!(
parsed.true_fitness_calls.is_none(),
"true_fitness_calls must be None when absent from checkpoint JSON, got {:?}",
parsed.true_fitness_calls
);
}
fn make_range_alleles() -> Vec<RangeGenotype<f64>> {
vec![RangeGenotype::new(0, vec![(0.0_f64, 1.0_f64)], 0.0_f64)]
}
fn build_surrogate_ga(
model: Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>,
fraction: f64,
) -> Result<Ga<RangeChromosome<f64>>, genetic_algorithms::error::GaError> {
let alleles = make_range_alleles();
let alleles_clone = alleles.clone();
Ga::new()
.with_surrogate(model, fraction)
.with_population_size(10)
.with_chromosome_length(ChromosomeLength::Fixed(2))
.with_alleles(alleles)
.with_initialization_fn(move |n, _| range_random_initialization(n, Some(&alleles_clone)))
.with_fitness_fn(|_dna: &[RangeGenotype<f64>]| 1.0)
.with_selection_method(Selection::Tournament)
.with_crossover_method(Crossover::SinglePoint)
.with_mutation_method(Mutation::Gaussian(GaussianParams { sigma: None }))
.with_survivor_method(Survivor::Fitness)
.with_max_generations(1)
.with_problem_solving(ProblemSolving::Maximization)
.build()
}
#[test]
fn invalid_fraction_zero_rejected() {
use genetic_algorithms::error::GaError;
let model =
Arc::new(ZeroSurrogate) as Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>;
let result = build_surrogate_ga(model, 0.0);
assert!(result.is_err(), "build() must fail for fraction=0.0");
match result.err().unwrap() {
GaError::ConfigurationError(msg) => {
assert!(
msg.contains("prescreening_fraction"),
"error message must contain 'prescreening_fraction', got: {}",
msg
);
}
e => panic!("Expected ConfigurationError, got: {:?}", e),
}
}
#[test]
fn invalid_fraction_over_one_rejected() {
use genetic_algorithms::error::GaError;
let model =
Arc::new(ZeroSurrogate) as Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>;
let result = build_surrogate_ga(model, 1.5);
assert!(result.is_err(), "build() must fail for fraction=1.5");
match result.err().unwrap() {
GaError::ConfigurationError(_) => {}
e => panic!("Expected ConfigurationError, got: {:?}", e),
}
}
#[test]
fn boundary_fraction_one_accepted() {
let model =
Arc::new(ZeroSurrogate) as Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>;
let result = build_surrogate_ga(model, 1.0);
assert!(
result.is_ok(),
"build() must succeed for fraction=1.0, got: {:?}",
result.err()
);
}
use genetic_algorithms::fitness::BatchFitnessEvaluator;
use std::sync::atomic::AtomicU64;
struct IdentitySurrogate;
impl SurrogateModel<RangeChromosome<f64>> for IdentitySurrogate {
fn predict(&self, chromosome: &RangeChromosome<f64>) -> f64 {
chromosome.fitness()
}
}
struct MaxSliceLengthEvaluator {
max_len: Arc<AtomicU64>,
}
impl BatchFitnessEvaluator<RangeChromosome<f64>> for MaxSliceLengthEvaluator {
fn evaluate_batch(&self, chromosomes: &[RangeChromosome<f64>]) -> Vec<f64> {
let len = chromosomes.len() as u64;
self.max_len.fetch_max(len, Ordering::SeqCst);
vec![1.0; chromosomes.len()]
}
}
fn build_runtime_ga(
model: Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>,
fraction: f64,
generations: usize,
) -> Ga<RangeChromosome<f64>> {
let alleles = make_range_alleles();
let alleles_clone = alleles.clone();
Ga::new()
.with_surrogate(model, fraction)
.with_population_size(20)
.with_chromosome_length(ChromosomeLength::Fixed(2))
.with_alleles(alleles)
.with_initialization_fn(move |n, _| range_random_initialization(n, Some(&alleles_clone)))
.with_fitness_fn(|_dna: &[RangeGenotype<f64>]| 1.0)
.with_selection_method(Selection::Tournament)
.with_crossover_method(Crossover::SinglePoint)
.with_mutation_method(Mutation::Gaussian(GaussianParams { sigma: None }))
.with_survivor_method(Survivor::Fitness)
.with_max_generations(generations)
.with_problem_solving(ProblemSolving::Maximization)
.build()
.expect("build_runtime_ga must succeed")
}
#[test]
fn ga_with_surrogate_runs() {
let model =
Arc::new(IdentitySurrogate) as Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>;
let mut ga = build_runtime_ga(model, 0.5, 5);
let result = ga.run();
assert!(
result.is_ok(),
"ga.run() must succeed with surrogate: {:?}",
result.err()
);
}
#[test]
fn prescreening_fraction_reduces_evaluations() {
let model =
Arc::new(IdentitySurrogate) as Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>;
let mut ga = build_runtime_ga(model, 0.5, 5);
ga.run().expect("run must succeed");
let stats = ga.stats();
assert!(!stats.is_empty(), "stats must be non-empty after run");
let found = stats.iter().any(|s| {
if let Some(calls) = s.true_fitness_calls {
calls <= 20 } else {
false
}
});
assert!(
found,
"at least one generation must have true_fitness_calls = Some(n)"
);
let reduced = stats
.iter()
.any(|s| s.true_fitness_calls.is_some_and(|c| c <= 10));
assert!(
reduced,
"at least one generation should have true_fitness_calls <= 10 with fraction=0.5 and pop=20; got: {:?}",
stats.iter().map(|s| s.true_fitness_calls).collect::<Vec<_>>()
);
}
#[test]
fn true_fitness_calls_populated_in_stats() {
let model =
Arc::new(IdentitySurrogate) as Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>;
let mut ga = build_runtime_ga(model, 0.5, 5);
ga.run().expect("run must succeed");
for (i, stat) in ga.stats().iter().enumerate() {
assert!(
stat.true_fitness_calls.is_some(),
"generation {i}: true_fitness_calls must be Some when surrogate is configured"
);
}
}
#[test]
fn true_fitness_calls_none_without_surrogate() {
let alleles = make_range_alleles();
let alleles_clone = alleles.clone();
let mut ga: Ga<RangeChromosome<f64>> = Ga::new()
.with_population_size(20)
.with_chromosome_length(ChromosomeLength::Fixed(2))
.with_alleles(alleles)
.with_initialization_fn(move |n, _| range_random_initialization(n, Some(&alleles_clone)))
.with_fitness_fn(|_dna: &[RangeGenotype<f64>]| 1.0)
.with_selection_method(Selection::Tournament)
.with_crossover_method(Crossover::SinglePoint)
.with_mutation_method(Mutation::Gaussian(GaussianParams { sigma: None }))
.with_survivor_method(Survivor::Fitness)
.with_max_generations(5)
.with_problem_solving(ProblemSolving::Maximization)
.build()
.expect("build must succeed");
ga.run().expect("run must succeed");
for (i, stat) in ga.stats().iter().enumerate() {
assert!(
stat.true_fitness_calls.is_none(),
"generation {i}: true_fitness_calls must be None when no surrogate is configured"
);
}
}
#[test]
fn surrogate_with_batch_evaluator_composes() {
let max_len = Arc::new(AtomicU64::new(0));
let evaluator = Arc::new(MaxSliceLengthEvaluator {
max_len: Arc::clone(&max_len),
});
let model =
Arc::new(IdentitySurrogate) as Arc<dyn SurrogateModel<RangeChromosome<f64>> + Send + Sync>;
let alleles = make_range_alleles();
let alleles_clone = alleles.clone();
let mut ga: Ga<RangeChromosome<f64>> = Ga::new()
.with_surrogate(model, 0.5)
.with_batch_evaluator(evaluator)
.with_population_size(20)
.with_chromosome_length(ChromosomeLength::Fixed(2))
.with_alleles(alleles)
.with_initialization_fn(move |n, _| range_random_initialization(n, Some(&alleles_clone)))
.with_selection_method(Selection::Tournament)
.with_crossover_method(Crossover::SinglePoint)
.with_mutation_method(Mutation::Gaussian(GaussianParams { sigma: None }))
.with_survivor_method(Survivor::Fitness)
.with_max_generations(5)
.with_problem_solving(ProblemSolving::Maximization)
.build()
.expect("build must succeed");
ga.run().expect("run must succeed");
let max_received = max_len.load(Ordering::SeqCst);
assert!(
max_received > 0,
"batch evaluator must have been called at least once"
);
for (i, stat) in ga.stats().iter().enumerate() {
assert!(
stat.true_fitness_calls.is_some(),
"generation {i}: true_fitness_calls must be Some when surrogate is configured"
);
let calls = stat.true_fitness_calls.unwrap();
assert!(
calls <= 10,
"generation {i}: true_fitness_calls ({calls}) must be ≤ 10 (half of pop=20 with fraction=0.5)"
);
}
}