use evolve::{
algorithm::EvolutionaryAlgorithm,
collector::standard::Standard,
experiment::Experiment,
fitness::{Maximize, Minimize},
initialization::Random,
operators::sequential::{
combinator::{Combine, Fill, Pipeline, Repeat, Weighted},
crossover::SinglePoint,
mutation::RandomReset,
selection::Tournament,
},
termination::MaxGenerations,
};
use rand::SeedableRng;
use std::num::NonZero;
fn nz(n: usize) -> NonZero<usize> {
NonZero::new(n).unwrap()
}
fn nz16(n: u16) -> NonZero<u16> {
NonZero::new(n).unwrap()
}
#[test]
fn build_with_maximize() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(RandomReset::new()),
nz(10),
rand::rng(),
Maximize,
);
}
#[test]
fn build_with_minimize() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(RandomReset::new()),
nz(10),
rand::rng(),
Minimize,
);
}
#[test]
fn build_with_closure_comparator() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(RandomReset::new()),
nz(10),
rand::rng(),
|a: &u16, b: &u16| a > b,
);
}
#[test]
fn build_with_pipeline() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 4]| g.iter().map(|x| *x as u32).sum::<u32>(),
Fill::from_population_size(Pipeline::new((
Tournament::new(nz(3)),
SinglePoint::new(),
RandomReset::new(),
))),
nz(10),
rand::rng(),
Maximize,
);
}
#[test]
fn build_with_combine() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(Combine::new((RandomReset::new(), RandomReset::new()))),
nz(10),
rand::rng(),
Maximize,
);
}
#[test]
fn build_with_repeat() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Repeat::new(RandomReset::new(), 10),
nz(10),
rand::rng(),
Maximize,
);
}
#[test]
fn build_with_weighted() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(Weighted::new((
(RandomReset::new(), nz16(3)),
(RandomReset::new(), nz16(1)),
))),
nz(10),
rand::rng(),
Maximize,
);
}
#[test]
fn build_with_fixed_size_fill() {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_fixed_size(RandomReset::new(), 20),
nz(10),
rand::rng(),
Maximize,
);
}
#[test]
fn maximize_improves_over_generations() {
let fitness_fn = |g: &[u8; 4]| g.iter().map(|x| *x as u32).sum::<u32>();
let ops = Fill::from_population_size(Pipeline::new((
Combine::new((
Tournament::new(nz(3)),
Tournament::new(nz(3)),
)),
SinglePoint::new(),
RandomReset::new(),
)));
let mut ga_short = EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(1),
fitness_fn,
ops,
nz(200),
rand::rngs::SmallRng::seed_from_u64(42),
Maximize,
);
let ops = Fill::from_population_size(Pipeline::new((
Combine::new((
Tournament::new(nz(3)),
Tournament::new(nz(3)),
)),
SinglePoint::new(),
RandomReset::new(),
)));
let mut ga_long = EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(200),
fitness_fn,
ops,
nz(200),
rand::rngs::SmallRng::seed_from_u64(42),
Maximize,
);
let short_best = *ga_short
.run()
.population()
.best(&fitness_fn, &Maximize)
.fitness(&fitness_fn);
let long_best = *ga_long
.run()
.population()
.best(&fitness_fn, &Maximize)
.fitness(&fitness_fn);
assert!(
long_best >= short_best,
"200 generations ({long_best}) should be >= 1 generation ({short_best})"
);
}
#[test]
fn minimize_finds_low_fitness() {
let fitness_fn = |g: &[u8; 2]| g[0] as u16 + g[1] as u16;
let mut ga = EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(200),
fitness_fn,
Fill::from_population_size(RandomReset::new()),
nz(200),
rand::rng(),
Minimize,
);
let best = ga.run();
let best_ind = best.population().best(&fitness_fn, &Minimize);
assert!(
*best_ind.fitness(&fitness_fn) < 100,
"expected low fitness, got {}",
best_ind.fitness(&fitness_fn)
);
}
#[test]
fn full_pipeline_runs_to_completion() {
let fitness_fn = |g: &[u8; 4]| g.iter().map(|x| *x as u32).sum::<u32>();
let mut ga = EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(50),
fitness_fn,
Fill::from_population_size(Pipeline::new((
Combine::new((
Tournament::new(nz(3)),
Tournament::new(nz(3)),
)),
SinglePoint::new(),
RandomReset::new(),
))),
nz(100),
rand::rng(),
Maximize,
);
let best = ga.run();
assert!(
*best
.population()
.best(&fitness_fn, &Maximize)
.fitness(&fitness_fn)
> 0
);
}
#[test]
fn zero_generations_returns_initial_best() {
let fitness_fn = |g: &[u8; 2]| g[0] as u16 + g[1] as u16;
let mut ga = EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(0),
fitness_fn,
Fill::from_population_size(RandomReset::new()),
nz(50),
rand::rng(),
Maximize,
);
let best = ga.run();
assert!(
*best
.population()
.best(&fitness_fn, &Maximize)
.fitness(&fitness_fn)
> 0
);
}
#[test]
fn weighted_pipeline_with_selection_and_mutation() {
let fitness_fn = |g: &[u8; 4]| g.iter().map(|x| *x as u32).sum::<u32>();
let mut ga = EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(50),
fitness_fn,
Fill::from_population_size(Weighted::new((
(
Pipeline::new((
Combine::new((
Tournament::new(nz(3)),
Tournament::new(nz(3)),
)),
SinglePoint::new(),
RandomReset::new(),
)),
nz16(3),
),
(RandomReset::new(), nz16(1)),
))),
nz(100),
rand::rng(),
Maximize,
);
let best = ga.run();
assert!(
*best
.population()
.best(&fitness_fn, &Maximize)
.fitness(&fitness_fn)
> 0
);
}
#[test]
fn run_with_collector() {
use evolve::collector::Collector;
use evolve::core::state::State;
use evolve::fitness::{FitnessComparator, FitnessEvaluator};
struct Counter {
started: bool,
generations: usize,
ended: bool,
}
struct CounterResult {
started: bool,
generations: usize,
ended: bool,
population_len: usize,
}
impl<G, F, Fe, C> Collector<G, F, Fe, C> for Counter
where
Fe: FitnessEvaluator<G, F>,
C: FitnessComparator<F>,
{
type Result = CounterResult;
fn on_start(&mut self, _: &State<G, F>, _: &Fe, _: &C) {
self.started = true;
}
fn on_generation(&mut self, _: &State<G, F>, _: &Fe, _: &C) {
self.generations += 1;
}
fn on_end(&mut self, _: &State<G, F>, _: &Fe, _: &C) {
self.ended = true;
}
fn finalize(self, state: State<G, F>) -> Self::Result {
CounterResult {
started: self.started,
generations: self.generations,
ended: self.ended,
population_len: state.population().len(),
}
}
}
let mut ga = EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(5),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(RandomReset::new()),
nz(50),
rand::rng(),
Maximize,
);
let result = ga.run_with(Counter {
started: false,
generations: 0,
ended: false,
});
assert!(result.started);
assert_eq!(result.generations, 5);
assert!(result.ended);
assert!(result.population_len > 0);
}
#[test]
fn builder_with_all_fields() {
let mut ga = EvolutionaryAlgorithm::builder(nz(50))
.initializer(Random::new())
.termination(MaxGenerations::new(10))
.fitness(|g: &[u8; 2]| g[0] as u16 + g[1] as u16)
.operators(Fill::from_population_size(RandomReset::new()))
.rng(rand::rng())
.comparator(Maximize)
.build();
let result = ga.run();
assert!(result.population().len() > 0);
}
#[test]
fn builder_with_minimize() {
let mut ga = EvolutionaryAlgorithm::builder(nz(50))
.initializer(Random::new())
.termination(MaxGenerations::new(10))
.fitness(|g: &[u8; 2]| g[0] as u16 + g[1] as u16)
.operators(Fill::from_population_size(RandomReset::new()))
.rng(rand::rng())
.comparator(Minimize)
.build();
let result = ga.run();
assert!(result.population().len() > 0);
}
#[test]
fn builder_with_pipeline() {
let mut ga = EvolutionaryAlgorithm::builder(nz(100))
.initializer(Random::new())
.termination(MaxGenerations::new(10))
.fitness(|g: &[u8; 4]| g.iter().map(|x| *x as u32).sum::<u32>())
.operators(Fill::from_population_size(Pipeline::new((
Combine::new((
Tournament::new(nz(3)),
Tournament::new(nz(3)),
)),
SinglePoint::new(),
RandomReset::new(),
))))
.rng(rand::rng())
.comparator(Maximize)
.build();
let result = ga.run();
assert!(result.population().len() > 0);
}
#[test]
fn builder_fields_in_any_order() {
let mut ga = EvolutionaryAlgorithm::builder(nz(50))
.comparator(Maximize)
.rng(rand::rng())
.operators(Fill::from_population_size(RandomReset::new()))
.fitness(|g: &[u8; 2]| g[0] as u16 + g[1] as u16)
.termination(MaxGenerations::new(5))
.initializer(Random::new())
.build();
let result = ga.run();
assert!(result.population().len() > 0);
}
#[test]
fn ga_with_variable_length_genome() {
use evolve::initialization::RangedRandom;
let fitness_fn = |g: &Vec<u8>| g.iter().map(|x| *x as u32).sum::<u32>();
let mut ga = EvolutionaryAlgorithm::new(
RangedRandom::<u8>::new(5..20),
MaxGenerations::new(50),
fitness_fn,
Fill::from_population_size(Pipeline::new((
Combine::new((
Tournament::new(nz(3)),
Tournament::new(nz(3)),
)),
SinglePoint::<u8>::new(),
RandomReset::<u8>::new(),
))),
nz(100),
rand::rng(),
Maximize,
);
let result = ga.run();
let best = *result
.population()
.best(&fitness_fn, &Maximize)
.fitness(&fitness_fn);
assert!(
best > 100,
"variable-length GA should find good solutions, got {best}"
);
}
#[test]
fn experiment_runs_multiple_trials() {
let experiment = Experiment::new(
|| {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(10),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(RandomReset::new()),
NonZero::new(20).unwrap(),
rand::rng(),
Maximize,
)
},
3,
|| Standard::default(),
);
let results = experiment.run();
assert_eq!(results.len(), 3);
for result in &results {
assert_eq!(result.generations(), 10);
assert!(!result.best_fitness().is_empty());
}
}
#[test]
fn experiment_with_custom_collector() {
use evolve::collector::basic::{self, Basic};
let experiment = Experiment::new(
|| {
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(10),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(RandomReset::new()),
NonZero::new(20).unwrap(),
rand::rng(),
Maximize,
)
},
3,
|| Basic::new(),
);
let results: Vec<basic::RunResult<[u8; 2], u16>> = experiment.run();
assert_eq!(results.len(), 3);
for result in &results {
assert_eq!(result.generations(), 10);
}
}
#[test]
fn factory_trait_on_struct() {
use rand::rngs::SmallRng;
use std::cell::Cell;
let seed = Cell::new(42u64);
let experiment = Experiment::new(
move || {
let rng = SmallRng::seed_from_u64(seed.get());
seed.set(seed.get() + 1);
EvolutionaryAlgorithm::new(
Random::new(),
MaxGenerations::new(10),
|g: &[u8; 2]| g[0] as u16 + g[1] as u16,
Fill::from_population_size(RandomReset::new()),
NonZero::new(20).unwrap(),
rng,
Maximize,
)
},
3,
|| Standard::default(),
);
let results = experiment.run();
assert_eq!(results.len(), 3);
}