use genalg::{
breeding::OrdinaryStrategy,
caching::CacheKey,
evolution::{
caching_challenge::CachingChallenge, Challenge, EvolutionLauncher, EvolutionOptions,
},
local_search::{AllIndividualsStrategy, HillClimbing, LocalSearchManager},
phenotype::Phenotype,
rng::RandomNumberGenerator,
selection::ElitistSelection,
};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
#[derive(Clone, Debug, PartialEq)]
struct TestPhenotype {
value: i32,
}
impl Phenotype for TestPhenotype {
fn crossover(&mut self, other: &Self) {
self.value = (self.value + other.value) / 2;
}
fn mutate(&mut self, _rng: &mut RandomNumberGenerator) {
self.value += 1;
}
}
impl CacheKey for TestPhenotype {
type Key = i32;
fn cache_key(&self) -> Self::Key {
self.value
}
}
#[derive(Clone)]
struct CostlyChallenge {
evaluations: Arc<AtomicUsize>,
}
impl CostlyChallenge {
fn new() -> Self {
Self {
evaluations: Arc::new(AtomicUsize::new(0)),
}
}
fn get_evaluations(&self) -> usize {
self.evaluations.load(Ordering::SeqCst)
}
}
impl Challenge<TestPhenotype> for CostlyChallenge {
fn score(&self, phenotype: &TestPhenotype) -> f64 {
self.evaluations.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(5));
-(phenotype.value as f64)
}
}
#[test]
fn test_direct_caching() {
let challenge = CostlyChallenge::new();
let cached_challenge = challenge.with_global_cache();
let phenotype = TestPhenotype { value: 42 };
let score1 = cached_challenge.score(&phenotype);
assert_eq!(score1, -42.0);
assert_eq!(challenge.get_evaluations(), 1);
let score2 = cached_challenge.score(&phenotype);
assert_eq!(score2, -42.0);
assert_eq!(challenge.get_evaluations(), 1);
let different_phenotype = TestPhenotype { value: 43 };
let score3 = cached_challenge.score(&different_phenotype);
assert_eq!(score3, -43.0);
assert_eq!(challenge.get_evaluations(), 2);
}
#[test]
fn test_thread_local_caching() {
let challenge = CostlyChallenge::new();
let cached_challenge = challenge.with_thread_local_cache();
let phenotype = TestPhenotype { value: 42 };
let score1 = cached_challenge.score(&phenotype);
assert_eq!(score1, -42.0);
assert_eq!(challenge.get_evaluations(), 1);
let score2 = cached_challenge.score(&phenotype);
assert_eq!(score2, -42.0);
assert_eq!(challenge.get_evaluations(), 1);
let thread_challenge = cached_challenge.clone();
let thread_phenotype = phenotype.clone();
let handle = std::thread::spawn(move || {
let score = thread_challenge.score(&thread_phenotype);
assert_eq!(score, -42.0);
});
handle.join().unwrap();
assert_eq!(challenge.get_evaluations(), 2);
}
#[test]
fn test_evolution_with_caching() {
let breed_strategy = OrdinaryStrategy::default();
let selection_strategy = ElitistSelection::default();
let hill_climbing = HillClimbing::new(5, 3).unwrap();
let app_strategy = AllIndividualsStrategy::new();
let challenge = CostlyChallenge::new();
let options = EvolutionOptions::builder()
.num_generations(5)
.population_size(10)
.num_offspring(15)
.build();
let starting_value = TestPhenotype { value: 100 };
let launcher = EvolutionLauncher::new(
breed_strategy.clone(),
selection_strategy.clone(),
Some(LocalSearchManager::new(
hill_climbing.clone(),
app_strategy.clone(),
)),
challenge.clone(),
);
let without_cache = launcher
.configure(options.clone(), starting_value.clone())
.run()
.unwrap();
let evaluations_without_cache = challenge.get_evaluations();
let challenge = CostlyChallenge::new();
let cached_challenge = challenge.with_global_cache();
let launcher_with_cache = EvolutionLauncher::new(
breed_strategy.clone(),
selection_strategy.clone(),
Some(LocalSearchManager::new(
hill_climbing.clone(),
app_strategy.clone(),
)),
cached_challenge,
);
let with_cache = launcher_with_cache
.configure(options.clone(), starting_value.clone())
.run()
.unwrap();
let evaluations_with_cache = challenge.get_evaluations();
println!(
"Evaluations without cache: {}, with cache: {}",
evaluations_without_cache, evaluations_with_cache
);
assert!(
evaluations_with_cache < evaluations_without_cache,
"Caching should reduce the number of evaluations"
);
println!(
"Score without cache: {}, with cache: {}",
without_cache.score, with_cache.score
);
assert!(
without_cache.score < 0.0,
"Without cache score should be negative"
);
assert!(
with_cache.score < 0.0,
"With cache score should be negative"
);
}
#[test]
fn test_cache_sharing() {
let challenge = CostlyChallenge::new();
let cached_challenge1 = challenge.with_global_cache();
let phenotype = TestPhenotype { value: 42 };
let score1 = cached_challenge1.score(&phenotype);
assert_eq!(score1, -42.0);
assert_eq!(challenge.get_evaluations(), 1);
let cached_challenge2 = challenge.with_global_cache();
let score2 = cached_challenge2.score(&phenotype);
assert_eq!(score2, -42.0);
assert_eq!(challenge.get_evaluations(), 2); }